mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 491a71759a | |||
| 2336beb6c4 | |||
| 513e44a5f5 | |||
| 0e7913f9f7 | |||
| 5d0b21b7e0 | |||
| e808acf31d | |||
| c39966aae0 | |||
| bef6cc1918 | |||
| 4d47ac033f | |||
| 9b5db6c002 | |||
| 08be6d6743 | |||
| a4b94a84a0 | |||
| a3d747c88d | |||
| 30b7a56a94 | |||
| aa309e727a | |||
| a0578030d3 | |||
| 73bad21e10 | |||
| 49928ebffd | |||
| a00b1a73af | |||
| fbfdb21cf1 | |||
| 018cdc012b |
@@ -904,9 +904,9 @@ mod web {
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
"insert" => {
|
||||
if !solidcraft_ui_egui::mesh_units::ask(&mut self.app, &path, true) {
|
||||
self.app.insert_path(&path);
|
||||
}
|
||||
// The staged loader, as Open uses: a large STEP insert keeps the page drawing
|
||||
// (it asks an STL's or OBJ's units first, as the synchronous insert did).
|
||||
self.app.insert_path_async(&path);
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
"download" => match vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES) {
|
||||
|
||||
@@ -890,7 +890,7 @@
|
||||
"Variable Radius": "نصف قطر متغير",
|
||||
"Variable radius": "نصف قطر متغير",
|
||||
"Velocity": "السرعة المتجهة",
|
||||
"Vertical": "عمودي",
|
||||
"Vertical": "رأسي",
|
||||
"Vertices": "الرؤوس",
|
||||
"View": "عرض",
|
||||
"View navigator": "متصفح العرض",
|
||||
@@ -2658,7 +2658,7 @@
|
||||
"prefs.devices": "الفأرة ثلاثية الأبعاد",
|
||||
"prefs.dialog_buttons": "أزرار مربعات الحوار",
|
||||
"prefs.dialog_first_letter_buttons": "أزرار نوافذ الحوار تستجيب لحرفها الأول",
|
||||
"prefs.dialog_first_letter_buttons_help": "عند فتح نافذة حوار، كتابة الحرف الأول من تسمية الزر تضغطه: حرف «ا» يضغط زر «إلغاء» مثلًا. الحرف الذي يناسب زرَّين لا يفعل شيئًا، وحقل النص المركَّز عليه يحتفظ بالتضغطة. عند الإيقاف: الكتابة لا تفعل شيئًا للأزرار، كما كان سابقًا.",
|
||||
"prefs.dialog_first_letter_buttons_help": "عند فتح نافذة حوار، تضغط كتابةُ الحرف الأول من تسمية الزر ذلك الزر: «م» لزر «موافق» و«إ» لزر «إلغاء». الحرف الذي يناسب زرَّين لا يفعل شيئًا، وحقل النص النشط يحتفظ بضغطة المفتاح. عند الإيقاف: الكتابة لا تؤثر في الأزرار، كما كان سابقًا.",
|
||||
"prefs.dialogs": "الحوارات",
|
||||
"prefs.dialogs_edge": "عند حافة العرض",
|
||||
"prefs.dialogs_left_column": "في العمود الأيسر",
|
||||
@@ -2686,7 +2686,7 @@
|
||||
"prefs.extrude_view_isometric": "التحويل إلى العرض المتساوي القياس عند فتح البثق",
|
||||
"prefs.extrude_view_return": "العودة إلى العرض السابق عند إغلاق البثق",
|
||||
"prefs.file_operations_wait": "انتظار فتح الملفات",
|
||||
"prefs.file_operations_wait_note": "أثناء فتح ملف أو إدراجه أو إعادة قراءة الروابط، ينتظر التصميم خلف بطاقة تعرض التقدم وزر الإلغاء. عند الإيقاف: يظهر الملف في لوحة العمليات ويمكنك متابعة العمل؛ يُطبَّق الإدراج بعد أن تنتهي مما تفعله، ويُفتح الملف المفتوح في الأثناء في علامة تبويب جديدة.",
|
||||
"prefs.file_operations_wait_note": "أثناء فتح ملف أو إدراجه أو إعادة قراءة الروابط، ينتظر التصميم خلف بطاقة تعرض تقدّم الملف وزر «إلغاء». عند الإيقاف: يظهر الملف في لوحة العمليات ويمكنك متابعة العمل؛ يُطبَّق الإدراج بعد أن تنتهي مما تفعله، والملف الذي تفتحه في هذه الأثناء يُفتح في علامة تبويب جديدة.",
|
||||
"prefs.fillet_legacy_radius": "استخدام نصف القطر الافتراضي السابق للتقويس المصمت",
|
||||
"prefs.fillet_legacy_radius.help": "معطّل افتراضيًا. عند التفعيل، يفتح التقويس المصمت الجديد على الرقم 2 مقروءًا بوحدات التصميم («2 بوصة» في تصميم بالبوصة)، كما كان سابقًا؛ وعند الإيقاف، يفتح على نصف القطر الافتراضي للأداة، 2 مم، معروضًا بوحدات التصميم («0.07874 بوصة» في تصميم بالبوصة).",
|
||||
"prefs.follows_os": "يتبع النظام",
|
||||
@@ -5443,7 +5443,7 @@
|
||||
"ui.construct_auto.no_fit": "اختر مراجع أخرى: لا توجد طريقة تبني من هذه المراجع.",
|
||||
"ui.construct_auto.pick": "اختر ما تبني منه: الطريقة تتبع اختياراتك.",
|
||||
"ui.construct_auto.refs": "المراجع",
|
||||
"ui.construct_auto.select": "اختر أولًا المراجع التي تبني منها",
|
||||
"ui.construct_auto.select": "حدد أولًا المراجع التي ستبني منها",
|
||||
"ui.construct_axes": "المحاور",
|
||||
"ui.construct_planes": "المستويات",
|
||||
"ui.construct_points": "النقاط",
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::schema::file::{
|
||||
RecoverResult, RecoveredDesign, RecoveryDiscardParams, RecoveryDiscardResult, RecoveryListParams, RecoveryListResult,
|
||||
};
|
||||
use crate::schema::insert_mesh::{InsertMeshParams, MeshInserted};
|
||||
use crate::schema::insert_step::{InsertStepParams, StepInserted};
|
||||
use crate::schema::new_design::{NewDesignParams, NewDesignResult};
|
||||
use crate::schema::save::{SaveAsParams, SaveParams, SaveResult};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -37,6 +38,7 @@ use crate::schema::{TypedContract, output_schema_for};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
const INSERT_MESH_CONTRACT: TypedContract<InsertMeshParams> = TypedContract::new();
|
||||
const INSERT_STEP_CONTRACT: TypedContract<InsertStepParams> = TypedContract::new();
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("file.new", "New Design", crate::checked!("file.new", new_doc))
|
||||
@@ -53,9 +55,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.contract(&OPEN_CONTRACT)
|
||||
.output(output_schema_for::<OpenResult>)
|
||||
.params("path: .sc design (or a .solidcraft design from an earlier release), .dxf/.svg drawing (opens as a new, unsaved design with an XY sketch, using sketch.insert_dxf/sketch.insert_svg units and defaults), or a .step/.stp/.igs/.fcstd/.3dm/.3mf/.stl/.obj/.glb/.gltf file (STEP, IGES, FreeCAD, Rhino, 3MF, STL, OBJ or glTF; opens as a new design holding the file as a component named after it, as file.insert_step and file.insert_mesh make), or a .ply/.xyz/.pts point cloud (opens as a new design holding it, as pointcloud.insert makes; its `units` and `max_points` as there); up?: z|y for a STEP, IGES, FreeCAD, Rhino or glTF file (see file.insert_step and file.insert_mesh); names?: auto|solid|product|instance for a STEP, IGES, FreeCAD or Rhino file (see file.insert_step); units?: for an STL or OBJ file (see file.insert_mesh); origin?: bool for a mesh file (see file.insert_mesh); folder?: bool for a STEP, IGES, FreeCAD or Rhino file (see file.insert_step); read_only?: bool (a design: saving to its own file is refused, file.save_as keeps the changes elsewhere) → open_elsewhere: {host, user, app, opened} when another SolidCraft has the design open (its lock file is fresh); newer_file: {format, reads_up_to, unknown_changes, not_understood, notice} and warnings when a newer SolidCraft saved it: it opens all the same, and what this version can't read is named there and kept by saving"),
|
||||
CommandSpec::new("file.insert_step", "Insert STEP", insert_step)
|
||||
CommandSpec::new("file.insert_step", "Insert STEP", crate::checked!("file.insert_step", insert_step))
|
||||
.at("SOLID", "INSERT")
|
||||
.icon("import")
|
||||
.contract(&INSERT_STEP_CONTRACT)
|
||||
.output(output_schema_for::<StepInserted>)
|
||||
.params("path: STEP, IGES, FreeCAD .FCStd or Rhino .3dm file (its bodies join the design as an Import base feature in a new component named after the file, placed in the active component; an assembly's products become nested components, a repeated part one component with an occurrence per placement, all held as one rigid group until component.unlock → component, component_name, occurrence, subcomponents?, rigid_joints?, group?: the folder); folder?: bool (default false; true: an import that brings more than one item into the history (its Import feature and the rigid joints holding its parts) goes into a new history folder named `Group #N`); name? (the feature's and the component's); up?: z|y (default z: the file's axes are kept; y: the file has Y up, so the import's component is turned a quarter turn about X, Y becoming Z → stand_up: its occurrence); names?: auto|solid|product|instance (default auto: keep meaningful solid names, replace placeholders with the nearest named product, else instance; solid: the solid's own name; product: the part's name; instance: the occurrence's name, else the product's); matrix?: [12 or 16 numbers] (where the new component's occurrence sits in the active component, after `up`: a rigid row-major 3x4 rotation-and-translation or 4x4 matrix, flat or as rows, as occurrence.move takes it; a scale, shear or mirror is refused before the file is read); tolerance? (see occurrence.move)"),
|
||||
CommandSpec::new("file.insert_mesh", "Insert Mesh", crate::checked!("file.insert_mesh", insert_mesh))
|
||||
.at("SOLID", "INSERT")
|
||||
@@ -287,23 +291,29 @@ fn add_step(s: &mut Session, path: &str, name: Option<&str>, options: &StepOptio
|
||||
let created = created_bodies(s, id);
|
||||
let folder = if options.folder { import_folder(s, id, &placed)? } else { None };
|
||||
let fname = s.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default();
|
||||
let mut out = json!({"path": path, "feature": id, "name": fname, "bodies": created, "created_bodies": created, "warnings": sf.warnings, "system": sf.import.system});
|
||||
if let Some((up, exporter)) = exporter_up(&sf.import) {
|
||||
out["exporter"] = json!(exporter);
|
||||
out["exporter_up"] = json!(up);
|
||||
}
|
||||
placed.report(&mut out);
|
||||
if let Some(turned) = turned {
|
||||
out["stand_up"] = json!(turned);
|
||||
}
|
||||
if let Some(folder) = folder {
|
||||
out["group"] = json!(folder);
|
||||
}
|
||||
if let Some(mark) = profiled {
|
||||
let report = crate::schema::diagnostics::ProfileReport::new(&mark.report(), 20);
|
||||
out["profile"] = serde_json::to_value(report).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
}
|
||||
Ok(out)
|
||||
let known = exporter_up(&sf.import);
|
||||
let reply = StepInserted {
|
||||
path: path.to_string(),
|
||||
feature: id,
|
||||
name: fname,
|
||||
bodies: created.clone(),
|
||||
created_bodies: created,
|
||||
warnings: sf.warnings,
|
||||
system: sf.import.system.clone(),
|
||||
exporter: known.map(|(_, exporter)| exporter.to_string()),
|
||||
exporter_up: known.map(|(up, _)| up.to_string()),
|
||||
component: placed.component,
|
||||
component_name: placed.name.clone(),
|
||||
occurrence: placed.occurrence,
|
||||
subcomponents: (placed.components > 0).then_some(placed.components),
|
||||
rigid_joints: (placed.components > 0).then_some(placed.joints.len()),
|
||||
stand_up: turned,
|
||||
group: folder,
|
||||
transform: None,
|
||||
profile: profiled.map(|mark| crate::schema::diagnostics::ProfileReport::new(&mark.report(), 20)),
|
||||
param_warnings: Vec::new(),
|
||||
};
|
||||
serde_json::to_value(reply).map_err(|e| EngineError::Other(format!("{path}: {e}")))
|
||||
}
|
||||
|
||||
/// An import that brings more than one item into the history (its Import feature and the rigid
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_doc::joints::{Joint, JointKind, JointOrigin, Snap};
|
||||
use solidcraft_doc::kernel::{StepImport, StepNode};
|
||||
@@ -187,7 +186,8 @@ fn node_key(imp: &StepImport, node: &StepNode, depth: usize) -> u64 {
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// The import's component, as the command reports it.
|
||||
/// The import's component, as the command reports it. The reply types in `schema/insert_mesh.rs`
|
||||
/// and `schema/insert_step.rs` are what the commands answer with these fields.
|
||||
pub(crate) struct Placed {
|
||||
pub component: u64,
|
||||
pub occurrence: Option<u64>,
|
||||
@@ -198,18 +198,6 @@ pub(crate) struct Placed {
|
||||
pub joints: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Placed {
|
||||
pub fn report(&self, out: &mut Value) {
|
||||
out["component"] = json!(self.component);
|
||||
out["component_name"] = json!(self.name);
|
||||
out["occurrence"] = json!(self.occurrence);
|
||||
if self.components > 0 {
|
||||
out["subcomponents"] = json!(self.components);
|
||||
out["rigid_joints"] = json!(self.joints.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The component an import makes: named `name`, in the active component at its origin.
|
||||
fn new_component(s: &mut Session, name: &str) -> Result<(u64, Option<u64>)> {
|
||||
let parent = s.active_component;
|
||||
|
||||
@@ -112,6 +112,7 @@ mod print_face;
|
||||
mod render;
|
||||
pub(crate) use render::{
|
||||
PreparedCapture, prepare_capture_with_metadata, read_render_request, render_cancel_params, render_form_refusal, render_saved_job_refusal,
|
||||
render_started,
|
||||
};
|
||||
pub(crate) use render_ui::batch_request;
|
||||
mod render_ui;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@ use crate::schema::render_jobs::{
|
||||
ExportFormat, ImageCopied, ImageExported, Job, JobProgressReply, JobState, PreparationCancelled, QueueReply, RenderCancelled, RenderProgress,
|
||||
ResultPinned,
|
||||
};
|
||||
use crate::schema::render_start::{Checked, Denoising, GpuChecked, IndirectClamp, RenderChecked, RenderStarted, RuntimeChecked};
|
||||
use crate::{Result, Session, params::bad, render_jobs::job_error};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -95,7 +96,7 @@ struct ProgressiveStart {
|
||||
pub(crate) struct Prepared {
|
||||
pub(crate) replace: bool,
|
||||
pub(crate) submission: Option<solidcraft_render::job::PreparedSubmission>,
|
||||
pub(crate) reply: Value,
|
||||
pub(crate) reply: RenderChecked,
|
||||
}
|
||||
|
||||
fn lower_led(input: &mut Start, command: &str) -> Result<()> {
|
||||
@@ -662,25 +663,31 @@ pub(crate) fn prepare_with_metadata(
|
||||
(true, false) => vec!["thin_sheet", "closed_box"],
|
||||
(false, _) => Vec::new(),
|
||||
};
|
||||
let mut reply = json!({"ignored_controls":ignored,"stage_panels":settings.stage.len(),"stage_transport":stage_transport, "cpu_budget":budget,"cpu_limits":budget.limits(),"environment":environment_info,"textures":request.textures.as_ref().map(|maps|maps.summary()),"retained_request_bytes":retained_request_bytes,"resolved":resolved,"admission":admission,"request":request,"scene_generation":s.revision,"host":host.kind(),"transparency":transparency,"refraction":refraction,"dielectric_boundaries":dielectric_boundaries});
|
||||
if !coats.is_empty() {
|
||||
reply["coats"] = json!(coats);
|
||||
}
|
||||
if let Some(options) = request.raytrace.as_ref().filter(|options| options.denoise) {
|
||||
reply["denoising"] = json!({
|
||||
"method":"edge_aware_atrous", "backend":"native_cpu", "passes":options.denoise_settings.passes,
|
||||
"steps":denoise_steps(options.denoise_settings.passes), "settings":options.denoise_settings,
|
||||
"diagnostics_requested":options.denoise_diagnostics,
|
||||
"outlier_policy":if options.denoise_settings.firefly_suppression {DENOISE_OUTLIER_POLICY} else {"disabled"},
|
||||
"optical_policy":if options.denoise_settings.optical_filter {DENOISE_OPTICAL_POLICY} else {DENOISE_OPAQUE_ONLY_POLICY},
|
||||
"guides":"deterministic_center_ray_albedo_normal_camera_relative_position_depth",
|
||||
"bypass":if options.denoise_settings.optical_filter {"unsupported or ambiguous optical paths, emitter, background and partial coverage"} else {"all optical paths, emitter, background and partial coverage"},
|
||||
"limitation":"biased spatial filter; optical guides do not decompose reflected and transmitted radiance or reconstruct depth-of-field correspondences",
|
||||
"raw_linear_accumulator_preserved":true,"raw_linear_exr_preserved":true,
|
||||
"output_stage":"linear_before_tone_map", "work_units":"one pixel visit per pass, each with at most 25 taps",
|
||||
"checkpoints":"every complete published pass uses the same filter"
|
||||
});
|
||||
}
|
||||
let denoising = request.raytrace.as_ref().filter(|options| options.denoise);
|
||||
let checked = Checked {
|
||||
resolved: retyped(command, &resolved)?,
|
||||
admission: retyped(command, &admission)?,
|
||||
request: retyped(command, request)?,
|
||||
scene_generation: s.revision,
|
||||
host: retyped(command, &host.kind())?,
|
||||
stage_panels: settings.stage.len(),
|
||||
environment: environment_info.as_ref().map(|info| retyped(command, info)).transpose()?,
|
||||
textures: request.textures.as_ref().map(|maps| retyped(command, &maps.summary())).transpose()?,
|
||||
transparency,
|
||||
refraction,
|
||||
dielectric_boundaries,
|
||||
coats: (!coats.is_empty()).then(|| retyped(command, &coats)).transpose()?,
|
||||
denoising: denoising.map(|options| denoising_reply(command, &options.denoise_settings, options.denoise_diagnostics, true)).transpose()?,
|
||||
param_warnings: Vec::new(),
|
||||
};
|
||||
let reply = RenderChecked::Runtime(RuntimeChecked {
|
||||
checked,
|
||||
ignored_controls: ignored,
|
||||
stage_transport,
|
||||
cpu_budget: retyped(command, &budget)?,
|
||||
cpu_limits: retyped(command, &budget.limits())?,
|
||||
retained_request_bytes,
|
||||
});
|
||||
Ok(Prepared { replace: input.replace, submission: Some(submission), reply })
|
||||
}
|
||||
|
||||
@@ -907,30 +914,40 @@ fn prepare_progressive(
|
||||
.and_then(|bytes| bytes.checked_add(retained_options_bytes))
|
||||
.filter(|bytes| *bytes <= host.job_limit_bytes())
|
||||
.ok_or_else(|| bad(command, "GPU work and retained metadata exceed the per-job limit"))?;
|
||||
let reply = json!({"stage_panels":settings.stage.len(),"resolved":resolved,"admission":admission,"request":request,"textures":request.textures.as_ref().map(|resources|resources.summary()),"scene_generation":s.revision,"host":host.kind(),"transparency":if refraction {"physical_transmission"} else if input.transparent_background {"coverage_alpha"} else if input.progressive {"opaque_only"} else {"sorted_alpha_approximation"},"options":options,"retained_options_bytes":retained_options_bytes,"environment":request.environment.as_ref().map(|source|source.info()),"refraction":refraction,"dielectric_boundaries": if resolved.executor.as_str() == "gpu-path" {vec!["thin_sheet","closed_box","closed_mesh"]} else {Vec::<&str>::new()}});
|
||||
let mut reply = reply;
|
||||
if !coats.is_empty() {
|
||||
reply["coats"] = json!(coats);
|
||||
}
|
||||
if options.denoise {
|
||||
reply["denoising"] = json!({
|
||||
"method":"edge_aware_atrous", "passes":options.denoise_settings.passes,
|
||||
"steps":denoise_steps(options.denoise_settings.passes), "settings":options.denoise_settings,
|
||||
"diagnostics_requested":options.denoise_diagnostics,
|
||||
"outlier_policy":if options.denoise_settings.firefly_suppression {DENOISE_OUTLIER_POLICY} else {"disabled"},
|
||||
"optical_policy":if options.denoise_settings.optical_filter {DENOISE_OPTICAL_POLICY} else {DENOISE_OPAQUE_ONLY_POLICY},
|
||||
"guides":"deterministic_center_ray_albedo_normal_position_depth",
|
||||
"bypass":if options.denoise_settings.optical_filter {"unsupported or ambiguous optical paths, emitter, background and partial coverage"} else {"all optical paths, emitter, background and partial coverage"},
|
||||
"limitation":"optical guides do not decompose reflected and transmitted radiance or reconstruct depth-of-field correspondences",
|
||||
"raw_linear_accumulator_preserved":true, "output_stage":"linear_before_tone_map"
|
||||
});
|
||||
}
|
||||
if let Some(limit) = options.clamp_indirect {
|
||||
reply["indirect_clamp"] = json!({
|
||||
"limit":limit, "from_bounce":1, "keeps_hue":true, "biased":true,
|
||||
"limitation":"dims real caustics and glints; the raw linear EXR master is clamped too; optical glass light connections resolved after the path are not clamped"
|
||||
});
|
||||
}
|
||||
let transparency = if refraction {
|
||||
"physical_transmission"
|
||||
} else if input.transparent_background {
|
||||
"coverage_alpha"
|
||||
} else if input.progressive {
|
||||
"opaque_only"
|
||||
} else {
|
||||
"sorted_alpha_approximation"
|
||||
};
|
||||
let dielectric_boundaries = if resolved.executor.as_str() == "gpu-path" { vec!["thin_sheet", "closed_box", "closed_mesh"] } else { Vec::new() };
|
||||
let checked = Checked {
|
||||
resolved: retyped(command, &resolved)?,
|
||||
admission: retyped(command, &admission)?,
|
||||
request: retyped(command, request)?,
|
||||
scene_generation: s.revision,
|
||||
host: retyped(command, &host.kind())?,
|
||||
stage_panels: settings.stage.len(),
|
||||
environment: request.environment.as_ref().map(|source| retyped(command, source.info())).transpose()?,
|
||||
textures: request.textures.as_ref().map(|resources| retyped(command, &resources.summary())).transpose()?,
|
||||
transparency,
|
||||
refraction,
|
||||
dielectric_boundaries,
|
||||
coats: (!coats.is_empty()).then(|| retyped(command, &coats)).transpose()?,
|
||||
denoising: options.denoise.then(|| denoising_reply(command, &options.denoise_settings, options.denoise_diagnostics, false)).transpose()?,
|
||||
param_warnings: Vec::new(),
|
||||
};
|
||||
let indirect_clamp = options.clamp_indirect.map(|limit| IndirectClamp {
|
||||
limit,
|
||||
from_bounce: 1,
|
||||
keeps_hue: true,
|
||||
biased: true,
|
||||
limitation: "dims real caustics and glints; the raw linear EXR master is clamped too; optical glass light connections resolved after the path are not clamped",
|
||||
});
|
||||
let reply = RenderChecked::Gpu(GpuChecked { checked, options: retyped(command, &options)?, retained_options_bytes, indirect_clamp });
|
||||
Ok(Prepared { replace: input.replace, submission: Some(submission), reply })
|
||||
}
|
||||
|
||||
@@ -949,8 +966,43 @@ fn ignored_controls(settings: &RenderSetup) -> Vec<&'static str> {
|
||||
ignored
|
||||
}
|
||||
|
||||
/// How the denoising filter of a CPU ray tracing or GPU request works, for its preflight reply.
|
||||
fn denoising_reply(command: &str, settings: &solidcraft_render::job::DenoiseSettings, diagnostics: bool, cpu: bool) -> Result<Denoising> {
|
||||
Ok(Denoising {
|
||||
method: "edge_aware_atrous",
|
||||
backend: cpu.then_some("native_cpu"),
|
||||
passes: settings.passes,
|
||||
steps: denoise_steps(settings.passes),
|
||||
settings: retyped(command, settings)?,
|
||||
diagnostics_requested: diagnostics,
|
||||
outlier_policy: if settings.firefly_suppression { DENOISE_OUTLIER_POLICY } else { "disabled" },
|
||||
optical_policy: if settings.optical_filter { DENOISE_OPTICAL_POLICY } else { DENOISE_OPAQUE_ONLY_POLICY },
|
||||
guides: if cpu {
|
||||
"deterministic_center_ray_albedo_normal_camera_relative_position_depth"
|
||||
} else {
|
||||
"deterministic_center_ray_albedo_normal_position_depth"
|
||||
},
|
||||
bypass: if settings.optical_filter {
|
||||
"unsupported or ambiguous optical paths, emitter, background and partial coverage"
|
||||
} else {
|
||||
"all optical paths, emitter, background and partial coverage"
|
||||
},
|
||||
limitation: if cpu {
|
||||
"biased spatial filter; optical guides do not decompose reflected and transmitted radiance or reconstruct depth-of-field correspondences"
|
||||
} else {
|
||||
"optical guides do not decompose reflected and transmitted radiance or reconstruct depth-of-field correspondences"
|
||||
},
|
||||
raw_linear_accumulator_preserved: true,
|
||||
raw_linear_exr_preserved: cpu.then_some(true),
|
||||
output_stage: "linear_before_tone_map",
|
||||
work_units: cpu.then_some("one pixel visit per pass, each with at most 25 taps"),
|
||||
checkpoints: cpu.then_some("every complete published pass uses the same filter"),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn preflight(s: &mut Session, params: &Value) -> Result<Value> {
|
||||
prepare(s, params, "render.preflight", &solidcraft_render::job::CancelToken::default()).map(|p| p.reply)
|
||||
let cmd = "render.preflight";
|
||||
super::typed_reply(cmd, prepare(s, params, cmd, &solidcraft_render::job::CancelToken::default())?.reply)
|
||||
}
|
||||
|
||||
pub(super) fn start(s: &mut Session, params: &Value) -> Result<Value> {
|
||||
@@ -964,11 +1016,28 @@ pub(super) fn start(s: &mut Session, params: &Value) -> Result<Value> {
|
||||
params.get("setup").and_then(Value::as_str).map(str::to_owned),
|
||||
)
|
||||
.map_err(job_error)?;
|
||||
let mut status = json!(status);
|
||||
if let (Some(out), Some(ignored)) = (status.as_object_mut(), p.reply.get("ignored_controls")) {
|
||||
out.insert("ignored_controls".into(), ignored.clone());
|
||||
}
|
||||
Ok(status)
|
||||
Ok(started("render.start", &status, &p.reply))
|
||||
}
|
||||
|
||||
/// What Render Image returns once its job is queued: the job's `status`, and for a runtime
|
||||
/// request the ignored controls of its preflight reply `checked`. The window's own preparation
|
||||
/// (`render_preparation`) returns it too.
|
||||
pub(crate) fn started(command: &str, status: &solidcraft_render::job::JobStatus, checked: &RenderChecked) -> Value {
|
||||
let ignored_controls = match checked {
|
||||
RenderChecked::Runtime(runtime) => Some(runtime.ignored_controls.clone()),
|
||||
RenderChecked::Gpu(_) => None,
|
||||
};
|
||||
// The job is queued already, so this never fails: should the status ever not read as a
|
||||
// `JobState`, the reply is the status as the render crate gives it, as before the reply type.
|
||||
let typed =
|
||||
retyped(command, status).and_then(|state| super::typed_reply(command, RenderStarted { state, ignored_controls: ignored_controls.clone() }));
|
||||
typed.unwrap_or_else(|_| {
|
||||
let mut reply = json!(status);
|
||||
if let (Some(out), Some(ignored)) = (reply.as_object_mut(), ignored_controls) {
|
||||
out.insert("ignored_controls".into(), json!(ignored));
|
||||
}
|
||||
reply
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! screen pixels, grid, selection highlights or tessellation diagonals are exported. The lines
|
||||
//! come from [`crate::mesh_lines`]; this command chooses the bodies and writes the DXF.
|
||||
|
||||
#[path = "view_dxf_arcs.rs"]
|
||||
mod arcs;
|
||||
#[path = "view_dxf_sketch.rs"]
|
||||
pub(super) mod sketch;
|
||||
|
||||
@@ -26,7 +28,7 @@ pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new(CMD, "Export Current Vi
|
||||
.noundo()
|
||||
.contract(&CONTRACT)
|
||||
.output(output_schema_for::<ViewDxfResult>)
|
||||
.params("camera: Camera (current view); hidden?: omit|dashed (default omit); tolerance?: 0.0001..1 mm (curve chord tolerance, default 0.01); zero?: target|origin (where the drawing's 0,0 is: the camera target, default, or the model origin as seen from the camera); path?: .dxf file (else returns dxf text). Visible placed bodies and shown sketches' profile curves (no construction geometry, points, dimensions or constraints); 2D LINE/LWPOLYLINE entities on VISIBLE/HIDDEN layers (sketch curves on SKETCH), mm; finished sketches are hidden behind bodies, the active sketch is drawn on top; perspective projected onto the camera target plane; no viewport crop or overlays.")];
|
||||
.params("camera: Camera (current view); hidden?: omit|dashed (default omit); tolerance?: 0.0001..1 mm (curve chord tolerance, default 0.01); curves?: lines|arcs (lines, default, writes the short lines the view is built from; arcs writes visible circles and arcs as CIRCLE and ARC); zero?: target|origin (where the drawing's 0,0 is: the camera target, default, or the model origin as seen from the camera); path?: .dxf file (else returns dxf text). Visible placed bodies and shown sketches' profile curves (no construction geometry, points, dimensions or constraints); 2D LINE/LWPOLYLINE entities on VISIBLE/HIDDEN layers (sketch curves on SKETCH), mm; finished sketches are hidden behind bodies, the active sketch is drawn on top; perspective projected onto the camera target plane; no viewport crop or overlays.")];
|
||||
|
||||
fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cam: Camera =
|
||||
@@ -54,6 +56,12 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(Value::String(v)) if v == "origin" => true,
|
||||
_ => return Err(bad(CMD, "zero must be target or origin")),
|
||||
};
|
||||
let as_arcs = match p.get("curves") {
|
||||
None => false,
|
||||
Some(Value::String(v)) if v == "lines" => false,
|
||||
Some(Value::String(v)) if v == "arcs" => true,
|
||||
_ => return Err(bad(CMD, "curves must be lines or arcs")),
|
||||
};
|
||||
let tolerance = match p.get("tolerance") {
|
||||
None => 0.01,
|
||||
Some(v) => v.as_f64().filter(|t| (0.0001..=1.0).contains(t)).ok_or_else(|| bad(CMD, "tolerance must be 0.0001..1 mm"))?,
|
||||
@@ -104,6 +112,9 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
entities.push(DxfEntity { layer: layer.into(), shape: DxfShape::Line(line.a, line.b) });
|
||||
}
|
||||
let mut entities = connect(entities);
|
||||
if as_arcs {
|
||||
entities = arcs::recognise(entities, tolerance);
|
||||
}
|
||||
let sketch = sketch_entities(drawing.sketch, dashed, &mut entities);
|
||||
let mut layers = vec![DxfLayer::new("VISIBLE", 7)];
|
||||
if dashed {
|
||||
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
//! Circles and arcs for the view drawing. The drawing's curved edges come from a tessellation, so
|
||||
//! a round hole arrives as a ring of short straight lines. A machining program or a CAD program
|
||||
//! can't pick that as a circle. This finds the chains of visible lines that are a tessellated
|
||||
//! circle or arc and writes them as `CIRCLE` and `ARC` entities; every other line stays a line.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use solidcore_record::Vec2;
|
||||
use solidcraft_geom::prelude::*;
|
||||
use solidcraft_io::{DxfEntity, DxfShape};
|
||||
|
||||
/// A closed ring needs this many segments to count as a circle, an open run this many to count as
|
||||
/// an arc. A hexagon or an octagon has every corner on a circle too; the chord check below turns
|
||||
/// them away, and these minimums keep a few lines from being read as a curve by chance.
|
||||
const MIN_CLOSED: usize = 8;
|
||||
const MIN_OPEN: usize = 4;
|
||||
|
||||
/// The visible lines of `entities` with the tessellated circles and arcs among them replaced by
|
||||
/// `CIRCLE` and `ARC`. `tolerance` is the chord tolerance the drawing was made with: a chain is a
|
||||
/// curve only when its chords sag from the circle by no more than about that much.
|
||||
pub(super) fn recognise(entities: Vec<DxfEntity>, tolerance: f64) -> Vec<DxfEntity> {
|
||||
let mut rest = Vec::new();
|
||||
let mut segments: Vec<(Vec2, Vec2)> = Vec::new();
|
||||
for e in entities {
|
||||
match e {
|
||||
DxfEntity { layer, shape: DxfShape::Line(a, b) } if layer == "VISIBLE" => segments.push((a, b)),
|
||||
other => rest.push(other),
|
||||
}
|
||||
}
|
||||
let extent = segments.iter().flat_map(|(a, b)| [a, b]).fold(1.0_f64, |s, p| s.max(p.x.abs()).max(p.y.abs()));
|
||||
// The two faces of a hole project to rings a hair apart; points that close are one point.
|
||||
snap(&mut segments, (tolerance * 1e-2).max(extent * 1e-9));
|
||||
let eps = extent * 1e-8;
|
||||
let key = |p: Vec2| [(p.x / eps).round() as i64, (p.y / eps).round() as i64];
|
||||
|
||||
// The same segment twice (the rim of a hole seen on both of its faces) must not make a
|
||||
// junction of every point of the ring.
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
segments.retain(|(a, b)| {
|
||||
let (ka, kb) = (key(*a), key(*b));
|
||||
ka != kb && seen.insert(if ka <= kb { (ka, kb) } else { (kb, ka) })
|
||||
});
|
||||
|
||||
let mut nodes: BTreeMap<[i64; 2], Vec<usize>> = BTreeMap::new();
|
||||
for (i, (a, b)) in segments.iter().enumerate() {
|
||||
nodes.entry(key(*a)).or_default().push(i);
|
||||
nodes.entry(key(*b)).or_default().push(i);
|
||||
}
|
||||
|
||||
let mut used = vec![false; segments.len()];
|
||||
let mut out = rest;
|
||||
for start in 0..segments.len() {
|
||||
if used.get(start).copied().unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let chain = chain_from(start, &segments, &nodes, &mut used, &key);
|
||||
match fit(&chain.points, chain.closed, tolerance) {
|
||||
Some(shape) => out.push(DxfEntity { layer: "VISIBLE".into(), shape }),
|
||||
None => {
|
||||
for w in chain.points.windows(2) {
|
||||
if let [a, b] = w {
|
||||
out.push(DxfEntity { layer: "VISIBLE".into(), shape: DxfShape::Line(*a, *b) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Moves every point to the first point seen within `reach` of it, so near-coincident points of the
|
||||
/// drawing become exactly equal.
|
||||
fn snap(segments: &mut [(Vec2, Vec2)], reach: f64) {
|
||||
let mut reps: Vec<Vec2> = Vec::new();
|
||||
let mut cells: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
|
||||
let mut at = |p: Vec2| -> Vec2 {
|
||||
let (cx, cy) = ((p.x / reach).floor() as i64, (p.y / reach).floor() as i64);
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for i in cells.get(&(cx + dx, cy + dy)).into_iter().flatten() {
|
||||
if let Some(r) = reps.get(*i)
|
||||
&& (*r - p).len() <= reach
|
||||
{
|
||||
return *r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reps.push(p);
|
||||
cells.entry((cx, cy)).or_default().push(reps.len() - 1);
|
||||
p
|
||||
};
|
||||
for (a, b) in segments.iter_mut() {
|
||||
*a = at(*a);
|
||||
*b = at(*b);
|
||||
}
|
||||
}
|
||||
|
||||
struct Chain {
|
||||
points: Vec<Vec2>,
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
/// The run of segments through `start`, continued while a point joins exactly two segments.
|
||||
fn chain_from(
|
||||
start: usize,
|
||||
segments: &[(Vec2, Vec2)],
|
||||
nodes: &BTreeMap<[i64; 2], Vec<usize>>,
|
||||
used: &mut [bool],
|
||||
key: &impl Fn(Vec2) -> [i64; 2],
|
||||
) -> Chain {
|
||||
if let Some(u) = used.get_mut(start) {
|
||||
*u = true;
|
||||
}
|
||||
let Some((a, b)) = segments.get(start).copied() else { return Chain { points: Vec::new(), closed: false } };
|
||||
let mut points = vec![a, b];
|
||||
let mut closed = false;
|
||||
for pass in 0..2 {
|
||||
while let Some(end) = points.last().copied() {
|
||||
let Some(joined) = nodes.get(&key(end)).filter(|n| n.len() == 2) else { break };
|
||||
let Some(next) = joined.iter().copied().find(|i| used.get(*i) == Some(&false)) else {
|
||||
// Both segments are used: the chain came back to where it began.
|
||||
closed = pass == 0 && points.len() > 2 && points.first().is_some_and(|f| key(*f) == key(end));
|
||||
break;
|
||||
};
|
||||
let Some((p, q)) = segments.get(next).copied() else { break };
|
||||
if let Some(u) = used.get_mut(next) {
|
||||
*u = true;
|
||||
}
|
||||
points.push(if key(p) == key(end) { q } else { p });
|
||||
}
|
||||
if closed {
|
||||
break;
|
||||
}
|
||||
points.reverse();
|
||||
}
|
||||
if !closed && points.len() > 2 && matches!((points.first(), points.last()), (Some(f), Some(l)) if key(*f) == key(*l)) {
|
||||
closed = true;
|
||||
}
|
||||
Chain { points, closed }
|
||||
}
|
||||
|
||||
/// The circle or arc a chain of points is a tessellation of, or `None`.
|
||||
fn fit(points: &[Vec2], closed: bool, tolerance: f64) -> Option<DxfShape> {
|
||||
let segments = points.len().saturating_sub(1);
|
||||
if segments < if closed { MIN_CLOSED } else { MIN_OPEN } {
|
||||
return None;
|
||||
}
|
||||
// A closed ring repeats its first point at the end; the fit takes each point once.
|
||||
let pts: &[Vec2] = if closed { points.get(..segments)? } else { points };
|
||||
let n = pts.len() as f64;
|
||||
let (mx, my) = (pts.iter().map(|p| p.x).sum::<f64>() / n, pts.iter().map(|p| p.y).sum::<f64>() / n);
|
||||
let (mut suu, mut suv, mut svv, mut suuu, mut svvv, mut suvv, mut svuu) = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
|
||||
for p in pts {
|
||||
let (u, v) = (p.x - mx, p.y - my);
|
||||
suu += u * u;
|
||||
suv += u * v;
|
||||
svv += v * v;
|
||||
suuu += u * u * u;
|
||||
svvv += v * v * v;
|
||||
suvv += u * v * v;
|
||||
svuu += v * u * u;
|
||||
}
|
||||
let det = suu * svv - suv * suv;
|
||||
if det.abs() < 1e-12 * (suu * svv).abs().max(1e-300) {
|
||||
return None; // a straight run
|
||||
}
|
||||
let (b1, b2) = (0.5 * (suuu + suvv), 0.5 * (svvv + svuu));
|
||||
let (uc, vc) = ((b1 * svv - b2 * suv) / det, (suu * b2 - suv * b1) / det);
|
||||
let r = (uc * uc + vc * vc + (suu + svv) / n).sqrt();
|
||||
let c = Vec2::new(mx + uc, my + vc);
|
||||
if !r.is_finite() || r <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
// Every point sits on the circle.
|
||||
let slack = 1e-4 * r + 1e-6;
|
||||
if pts.iter().any(|p| ((*p - c).len() - r).abs() > slack) {
|
||||
return None;
|
||||
}
|
||||
// Consecutive points go the same way round, and each chord sags from the circle by no more than
|
||||
// the drawing's own chord tolerance (twice it, for rounding): a polygon's sides sag far more.
|
||||
let angle = |p: Vec2| (p.y - c.y).atan2(p.x - c.x);
|
||||
let mut sweep = 0.0;
|
||||
let mut direction = 0.0_f64;
|
||||
for w in points.windows(2) {
|
||||
let (a, b) = (*w.first()?, *w.get(1)?);
|
||||
let mut d = angle(b) - angle(a);
|
||||
while d > std::f64::consts::PI {
|
||||
d -= std::f64::consts::TAU;
|
||||
}
|
||||
while d < -std::f64::consts::PI {
|
||||
d += std::f64::consts::TAU;
|
||||
}
|
||||
if d == 0.0 || (direction != 0.0 && d.signum() != direction) {
|
||||
return None;
|
||||
}
|
||||
direction = d.signum();
|
||||
let half_chord = (a.dist(b) / 2.0).min(r);
|
||||
if r - (r * r - half_chord * half_chord).sqrt() > 2.0 * tolerance {
|
||||
return None;
|
||||
}
|
||||
sweep += d;
|
||||
}
|
||||
if closed {
|
||||
return ((sweep.abs() - std::f64::consts::TAU).abs() < 1e-6).then_some(DxfShape::Circle { c, r });
|
||||
}
|
||||
if sweep.abs() >= std::f64::consts::TAU - 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let (first, last) = (angle(*points.first()?), angle(*points.last()?));
|
||||
// A DXF arc runs counter-clockwise from its first angle to its second.
|
||||
let (from, to) = if sweep > 0.0 { (first, last) } else { (last, first) };
|
||||
// A start a rounding error below 0 is 0, not 360.
|
||||
let degrees = |a: f64| Some(a.to_degrees().rem_euclid(360.0)).map(|d| if 360.0 - d < 1e-9 { 0.0 } else { d }).unwrap_or(0.0);
|
||||
Some(DxfShape::Arc { c, r, a0: degrees(from), a1: degrees(to) })
|
||||
}
|
||||
@@ -285,3 +285,69 @@ fn view_dxf_zero_origin_puts_the_model_origin_at_the_drawing_zero() {
|
||||
let err = s.execute(CMD, &json!({"camera": camera(StandardView::Top), "zero": "corner"})).unwrap_err().to_string();
|
||||
assert!(err.contains("zero must be target or origin"), "{err}");
|
||||
}
|
||||
|
||||
/// The lines of a regular polygon with `n` sides round `c`, from angle `from` to `to`.
|
||||
/// Over a whole turn the last line returns to the first point and the ring is closed.
|
||||
fn ring(c: Vec2, r: f64, n: usize, from: f64, to: f64) -> Vec<DxfEntity> {
|
||||
let at = |i: usize| {
|
||||
let t = from + (to - from) * i as f64 / n as f64;
|
||||
Vec2::new(c.x + r * t.cos(), c.y + r * t.sin())
|
||||
};
|
||||
(0..n).map(|i| DxfEntity { layer: "VISIBLE".into(), shape: DxfShape::Line(at(i), at(i + 1)) }).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_dxf_arcs_turn_a_tessellated_circle_into_a_circle_and_keep_polygons_as_lines() {
|
||||
let tau = std::f64::consts::TAU;
|
||||
let c = Vec2::new(3., -2.);
|
||||
// A fine ring is a circle; coarse rings (an octagon, even a 32-gon) sag too far from the
|
||||
// circle for the 0.01 mm the drawing was made with, so they are polygons and stay lines.
|
||||
let fine = arcs::recognise(ring(c, 10., 64, 0., tau), 0.01);
|
||||
assert_eq!(fine.len(), 1, "{fine:?}");
|
||||
let DxfShape::Circle { c: found, r } = &fine[0].shape else { panic!("{fine:?}") };
|
||||
assert!((*found - c).len() < 1e-9 && (r - 10.).abs() < 1e-9, "{fine:?}");
|
||||
for sides in [6, 8, 32] {
|
||||
let polygon = arcs::recognise(ring(c, 10., sides, 0., tau), 0.01);
|
||||
assert_eq!(polygon.len(), sides, "{sides} sides");
|
||||
assert!(polygon.iter().all(|e| matches!(e.shape, DxfShape::Line(..))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_dxf_arcs_turn_a_run_of_lines_into_an_arc_that_runs_counter_clockwise() {
|
||||
let c = Vec2::new(0., 0.);
|
||||
let quarter = std::f64::consts::FRAC_PI_2;
|
||||
// The run goes round clockwise, from 90 to 0 degrees: the DXF arc names 0 first, then 90.
|
||||
let run = arcs::recognise(ring(c, 10., 40, quarter, 0.), 0.01);
|
||||
assert_eq!(run.len(), 1, "{run:?}");
|
||||
let DxfShape::Arc { r, a0, a1, .. } = &run[0].shape else { panic!("{run:?}") };
|
||||
assert!((r - 10.).abs() < 1e-9 && a0.abs() < 1e-6 && (a1 - 90.).abs() < 1e-6, "{run:?}");
|
||||
// A straight run, and a few lines, are never an arc.
|
||||
let straight: Vec<DxfEntity> = (0..10)
|
||||
.map(|i| DxfEntity { layer: "VISIBLE".into(), shape: DxfShape::Line(Vec2::new(i as f64, 0.), Vec2::new(i as f64 + 1., 0.)) })
|
||||
.collect();
|
||||
assert_eq!(arcs::recognise(straight, 0.01).len(), 10);
|
||||
assert_eq!(arcs::recognise(ring(c, 10., 3, 0., quarter), 0.01).len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_dxf_curves_arcs_write_a_round_hole_as_one_circle_and_the_default_still_writes_lines() {
|
||||
let mut s = sample();
|
||||
// The sample's blind hole is hidden from above; this one goes right through.
|
||||
run(&mut s, "solid.cylinder", json!({"radius":5,"height":10,"base":[20,15,0],"operation":"cut","targets":["Plate"]}));
|
||||
let top = camera(StandardView::Top);
|
||||
let geometry = |v: &Value| solidcraft_io::read_dxf(v["dxf"].as_str().unwrap()).unwrap();
|
||||
let as_lines = geometry(&run(&mut s, CMD, json!({"camera": top})));
|
||||
assert!(as_lines.iter().all(|g| matches!(g, solidcraft_io::Geom2::Line(..))), "the default is unchanged");
|
||||
let as_arcs = geometry(&run(&mut s, CMD, json!({"camera": top, "curves": "arcs"})));
|
||||
let circles: Vec<_> = as_arcs.iter().filter_map(|g| if let solidcraft_io::Geom2::Circle(c, r) = g { Some((*c, *r)) } else { None }).collect();
|
||||
// The hole is centred on the camera target (20, 15), so it is the circle at 0,0.
|
||||
assert_eq!(circles.len(), 1, "{as_arcs:?}");
|
||||
assert!(circles[0].0.len() < 1e-3 && (circles[0].1 - 5.).abs() < 1e-4, "{circles:?}");
|
||||
// The plate's four sides stay four lines, and the hole's ring no longer adds dozens of lines.
|
||||
let lines = as_arcs.iter().filter(|g| matches!(g, solidcraft_io::Geom2::Line(..))).count();
|
||||
assert_eq!(lines, 4, "{as_arcs:?}");
|
||||
assert!(as_lines.len() > 40);
|
||||
let err = s.execute(CMD, &json!({"camera": top, "curves": "splines"})).unwrap_err().to_string();
|
||||
assert!(err.contains("curves must be lines or arcs"), "{err}");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ pub mod batch;
|
||||
pub(crate) mod batch_payload;
|
||||
|
||||
use crate::{Result, Session, cmd::PreparedCapture};
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::Value;
|
||||
use solidcraft_render::job::CancelToken;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
@@ -144,16 +144,12 @@ fn finish_inner(session: &mut Session, prepared: &mut Prepared, ticket: Option<b
|
||||
captured.check_cancel().map_err(crate::render_jobs::job_error)?;
|
||||
if prepared.kind == Kind::Check {
|
||||
prepared.completed = true;
|
||||
return Ok(std::mem::take(&mut prepared.capture.reply));
|
||||
return serde_json::to_value(&prepared.capture.reply).map_err(|e| crate::params::bad(prepared.kind.command(), e.to_string()));
|
||||
}
|
||||
let status = session
|
||||
.render_jobs
|
||||
.try_start_prepared_inner(session.revision, &mut prepared.capture.submission, prepared.capture.replace, prepared.shot.clone(), ticket)
|
||||
.map_err(crate::render_jobs::job_error)?;
|
||||
prepared.completed = true;
|
||||
let mut status = json!(status);
|
||||
if let (Some(out), Some(ignored)) = (status.as_object_mut(), prepared.capture.reply.get("ignored_controls")) {
|
||||
out.insert("ignored_controls".into(), ignored.clone());
|
||||
}
|
||||
Ok(status)
|
||||
Ok(crate::cmd::render_started(prepared.kind.command(), &status, &prepared.capture.reply))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Contract and reply type for `file.insert_step` in `cmd/file.rs` (#14918).
|
||||
//!
|
||||
//! The handler reads its parameters from the raw JSON, so each key here reads as the handler
|
||||
//! reads it and fails with the handler's message. `up` and `names` are the shared types `doc.open`
|
||||
//! describes, and `matrix` and `tolerance` those of `occurrence.move`. Whether the file is one of
|
||||
//! the kinds it takes, and whether a `matrix` is a rigid move, stay the handler's to refuse:
|
||||
//! `matrix::rigid` names what it rejects, in its own words.
|
||||
//!
|
||||
//! `path` is optional in the type and required in the schema, so a call without it gets the
|
||||
//! handler's "`path` is required" rather than the check's wording. The keys the handler does not
|
||||
//! read for a call are left unread, in the order it looks at them:
|
||||
//!
|
||||
//! - anything but `path` for a file whose extension it refuses, which it answers before reading
|
||||
//! another key;
|
||||
//! - `tolerance` without `matrix`, which is where the handler passes it to `matrix::rigid`.
|
||||
//!
|
||||
//! `tolerance` itself is read as given, as `occurrence.move` reads it: the schema gives it its
|
||||
//! shape only where the handler reads it, and `matrix::tolerance` is the one that refuses a bad
|
||||
//! value, in its own words.
|
||||
//!
|
||||
//! A value of the wrong type that the handler reads as if the key were left out (a `name` that is
|
||||
//! not text) is refused by the contract, as schemas.md has it: that difference is what warn and
|
||||
//! strict report. A call with several problems may get a different one first in strict than with
|
||||
//! the check off (the check goes key by key, the handler in its own order), except that a bad
|
||||
//! `path` is always the first.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator};
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::check::{located, whole};
|
||||
use super::component::{MatrixParam, MatrixTolerance};
|
||||
use super::diagnostics::ProfileReport;
|
||||
use super::file::{NamesParam, UpParam};
|
||||
use super::types::not_blank_pattern;
|
||||
|
||||
/// What `folder` says when it is neither true nor false, as `step_options` has it.
|
||||
const FOLDER_MESSAGE: &str =
|
||||
"`folder` must be true (an import that brings more than one item goes into a `Group #N` history folder) or false (no folder)";
|
||||
|
||||
/// `path` as `path_arg` reads it: text that is not blank and under 4096 bytes, refused with the
|
||||
/// handler's own message.
|
||||
fn path_key<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
|
||||
match Value::deserialize(d)? {
|
||||
Value::String(path) if crate::cmd::usable_path(&path) => Ok(Some(path)),
|
||||
_ => Err(D::Error::custom(whole("`path` is required"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// `folder`: true or false, `null` as left out, with the handler's message for anything else.
|
||||
fn folder_key<'de, D: Deserializer<'de>>(d: D) -> Result<Option<bool>, D::Error> {
|
||||
let value = Value::deserialize(d)?;
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
value.as_bool().map(Some).ok_or_else(|| D::Error::custom(whole(FOLDER_MESSAGE)))
|
||||
}
|
||||
|
||||
/// `matrix`: as given, `null` as left out, which is how the handler has it.
|
||||
fn matrix_key<'de, D: Deserializer<'de>>(d: D) -> Result<Option<MatrixParam>, D::Error> {
|
||||
let value = Value::deserialize(d)?;
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(MatrixParam(value)))
|
||||
}
|
||||
|
||||
/// The keys `file.insert_step` reads. See [`InsertStepParams`], which leaves out the keys the
|
||||
/// handler does not read for the call.
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields, transform = insert_step_schema)]
|
||||
pub struct InsertStepKeys {
|
||||
// Optional in the type and required in the schema (`insert_step_schema`): left out, the
|
||||
// handler refuses the call with its own message.
|
||||
/// The file to insert: `.step`, `.stp`, `.igs`, `.iges`, `.fcstd` or `.3dm` (any case), not
|
||||
/// blank and fewer than 4096 UTF-8 bytes. Any other kind of file is refused.
|
||||
#[serde(default, deserialize_with = "path_key", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "String")]
|
||||
pub path: Option<String>,
|
||||
/// The Import feature's name, and the new component's. Default (also when blank): the root
|
||||
/// product's name in the file, else the file's.
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
/// Which way is up in the file: `z` (its axes are kept, the default) or `y` (the file has Y
|
||||
/// up, so the import's component is turned a quarter turn about X and its Y becomes Z).
|
||||
#[serde(default)]
|
||||
pub up: Option<UpParam>,
|
||||
/// What the file's bodies are named after: `auto` (the default), `solid`, `product` or
|
||||
/// `instance`.
|
||||
#[serde(default)]
|
||||
pub names: Option<NamesParam>,
|
||||
/// true puts an import that brings more than one item into the history (its Import feature
|
||||
/// and the rigid joints holding its parts) into a new history folder named `Group #N`.
|
||||
/// Default: false.
|
||||
#[serde(default, deserialize_with = "folder_key", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "Option<bool>")]
|
||||
pub folder: Option<bool>,
|
||||
/// Where the import goes in the active component, as a rigid move applied after `up`: a
|
||||
/// row-major 3×4 (12 numbers) or 4×4 (16 numbers) matrix, flat or as rows, as
|
||||
/// `occurrence.move` takes it. The reply gives the placement it made as `transform`.
|
||||
#[serde(default, deserialize_with = "matrix_key", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(with = "Option<MatrixParam>")]
|
||||
pub matrix: Option<MatrixParam>,
|
||||
/// With `matrix` only: the largest accepted |RᵀR − I| per entry, above 0 and at most 0.001.
|
||||
/// Default: 1e-9. Read nowhere else, and `matrix::rigid` is the one that refuses both.
|
||||
#[serde(default)]
|
||||
pub tolerance: Option<MatrixTolerance>,
|
||||
}
|
||||
|
||||
/// Leaves out the keys the handler does not read for this call, in the order it looks at them.
|
||||
fn leave_unread(value: &mut Value) {
|
||||
let Some(keys) = value.as_object_mut() else { return };
|
||||
let Some(path) = keys.get("path").and_then(Value::as_str).filter(|path| crate::cmd::usable_path(path)) else {
|
||||
return;
|
||||
};
|
||||
if !solidcraft_io::is_step_path(path) {
|
||||
// A file of another kind is refused before any other key is read.
|
||||
for key in ["name", "up", "names", "folder", "matrix", "tolerance"] {
|
||||
keys.remove(key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if !keys.get("matrix").is_some_and(|matrix| !matrix.is_null()) {
|
||||
// `matrix::tolerance` is read only where `matrix` is.
|
||||
keys.remove("tolerance");
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a STEP, IGES, FreeCAD or Rhino file into the design as an Import base feature, in a new
|
||||
/// component named after the file and placed in the active component. An assembly's products
|
||||
/// become nested components held as one rigid group until `component.unlock`, and a part the file
|
||||
/// places more than once becomes one component with an occurrence per placement.
|
||||
#[derive(Debug)]
|
||||
pub struct InsertStepParams(pub InsertStepKeys);
|
||||
|
||||
impl<'de> Deserialize<'de> for InsertStepParams {
|
||||
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
||||
let mut value = Value::deserialize(d)?;
|
||||
// A missing or unusable `path` is the handler's first refusal, whatever else is wrong.
|
||||
if let Some(path) = value.as_object().map(|keys| keys.get("path"))
|
||||
&& !path.and_then(Value::as_str).is_some_and(crate::cmd::usable_path)
|
||||
{
|
||||
let only = json!({"path": path.cloned().unwrap_or(Value::Null)});
|
||||
return serde_path_to_error::deserialize::<_, InsertStepKeys>(&only).map(InsertStepParams).map_err(|e| D::Error::custom(located(&e)));
|
||||
}
|
||||
leave_unread(&mut value);
|
||||
serde_path_to_error::deserialize(&value).map(InsertStepParams).map_err(|e| D::Error::custom(located(&e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for InsertStepParams {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"InsertStepParams".into()
|
||||
}
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema = InsertStepKeys::json_schema(generator);
|
||||
schema.insert("description".into(), INSERT_STEP.into());
|
||||
schema
|
||||
}
|
||||
}
|
||||
|
||||
const INSERT_STEP: &str = "Insert a STEP, IGES, FreeCAD or Rhino file into the design as an Import base feature, in a new component named after the file and placed in the active component. An assembly's products become nested components held as one rigid group until `component.unlock`, and a part the file places more than once becomes one component with an occurrence per placement.";
|
||||
|
||||
/// `path` is required, as the handler has it (not blank, fewer than 4096 bytes; `maxLength`
|
||||
/// counts characters, so 4095 is as close as the schema gets). `tolerance` has its shape only
|
||||
/// where the handler reads it: with `matrix`, which is the handler's to refuse.
|
||||
fn insert_step_schema(schema: &mut Schema) {
|
||||
super::motion::require(schema, "path");
|
||||
let Some(props) = schema.get_mut("properties").and_then(Value::as_object_mut) else { return };
|
||||
props.insert(
|
||||
"path".into(),
|
||||
json!({
|
||||
"description": props.get("path").and_then(|path| path.get("description")).cloned().unwrap_or_default(),
|
||||
"type": "string",
|
||||
"maxLength": 4095,
|
||||
"pattern": not_blank_pattern(),
|
||||
}),
|
||||
);
|
||||
let shape = props.get_mut("tolerance").map(Value::take).unwrap_or_default();
|
||||
props.insert("tolerance".into(), json!({"description": shape.get("description").cloned().unwrap_or_default()}));
|
||||
schema.insert("anyOf".into(), json!([{"properties": {"matrix": {"type": "null"}}}, {"properties": {"tolerance": shape}}]));
|
||||
}
|
||||
|
||||
/// What `file.insert_step` returns.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct StepInserted {
|
||||
/// The file, as given.
|
||||
pub path: String,
|
||||
/// The Import feature's id.
|
||||
pub feature: u64,
|
||||
/// The feature's name.
|
||||
pub name: String,
|
||||
/// The bodies the import made, by name.
|
||||
pub bodies: Vec<String>,
|
||||
/// The same names as `bodies`.
|
||||
pub created_bodies: Vec<String>,
|
||||
/// What the file reader noticed and put right.
|
||||
pub warnings: Vec<String>,
|
||||
/// The program the file's header names as writing it.
|
||||
pub system: String,
|
||||
/// The exporter the file names, and which way up its files are known to be (`y` or `z`). Only
|
||||
/// for a program whose files are known to need standing up.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exporter: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exporter_up: Option<String>,
|
||||
/// The new component's id.
|
||||
pub component: u64,
|
||||
/// The new component's name.
|
||||
pub component_name: String,
|
||||
/// The component's occurrence in the active component; null when it has none.
|
||||
pub occurrence: Option<u64>,
|
||||
/// How many components were made below it (an assembly's parts and sub-assemblies). Only when
|
||||
/// there are some.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub subcomponents: Option<usize>,
|
||||
/// How many rigid joints hold its contents together. Only when `subcomponents` is given.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub rigid_joints: Option<usize>,
|
||||
/// With `up: y`: the occurrence that was turned.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub stand_up: Option<u64>,
|
||||
/// With `folder: true`: the history folder the import's feature and joints went into. Only
|
||||
/// when the import has joints to put in one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub group: Option<u64>,
|
||||
/// With `matrix`: the import's placement in the active component after the move, as the design
|
||||
/// stores a placement: 4 rows of 4 numbers, its translation in the last row.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub transform: Option<[[f64; 4]; 4]>,
|
||||
/// While the profiler records: the import's share of what it recorded, as `diagnostics.profile`
|
||||
/// reports it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<ProfileReport>,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "insert_step_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,333 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_kernel::{ExportBody, ExportProduct, StepHeader, box_solid, step_export_products};
|
||||
|
||||
use super::{InsertStepParams, StepInserted};
|
||||
use crate::Session;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
use crate::schema::{ParamCheck, input_schema_for, output_schema_for};
|
||||
|
||||
#[test]
|
||||
fn schemas_and_the_key_check_agree() {
|
||||
assert_keys(input_schema_for::<InsertStepParams>(), &["folder", "matrix", "name", "names", "path", "tolerance", "up"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_keeps_todays_errors() {
|
||||
let long = "x".repeat(4096);
|
||||
assert_strict_keeps_todays_errors(&[
|
||||
("file.insert_step", json!({})),
|
||||
("file.insert_step", json!({"path": ""})),
|
||||
("file.insert_step", json!({"path": " "})),
|
||||
("file.insert_step", json!({"path": 5})),
|
||||
("file.insert_step", json!({"path": null})),
|
||||
("file.insert_step", json!({"path": long})),
|
||||
// A file of another kind is refused before any other key is read.
|
||||
("file.insert_step", json!({"path": "x.stl"})),
|
||||
("file.insert_step", json!({"path": "x.stl", "up": "q", "names": 5, "folder": "x", "matrix": 5, "tolerance": 5})),
|
||||
("file.insert_step", json!({"path": "x.3mf", "name": 5})),
|
||||
("file.insert_step", json!({"path": "x.txt", "up": "y"})),
|
||||
("file.insert_step", json!({"path": "X.STL", "up": "y"})),
|
||||
// The keys of a CAD file, read in the handler's order.
|
||||
("file.insert_step", json!({"path": "a.step", "up": "q"})),
|
||||
("file.insert_step", json!({"path": "a.step", "up": 5})),
|
||||
("file.insert_step", json!({"path": "a.stp", "up": "y", "names": "bogus"})),
|
||||
("file.insert_step", json!({"path": "a.igs", "names": 5})),
|
||||
("file.insert_step", json!({"path": "a.iges", "names": []})),
|
||||
("file.insert_step", json!({"path": "a.fcstd", "folder": "yes"})),
|
||||
("file.insert_step", json!({"path": "a.3dm", "folder": 1})),
|
||||
// `matrix` is the handler's to read; `tolerance` only with it.
|
||||
("file.insert_step", json!({"path": "a.step", "matrix": "turn it"})),
|
||||
("file.insert_step", json!({"path": "a.step", "matrix": [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0]})),
|
||||
("file.insert_step", json!({"path": "a.step", "matrix": [1, 2, 3], "tolerance": "loose"})),
|
||||
("file.insert_step", json!({"path": "a.step", "matrix": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "tolerance": 0})),
|
||||
("file.insert_step", json!({"path": "a.step", "matrix": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "tolerance": 0.01})),
|
||||
// A file that isn't there.
|
||||
("file.insert_step", json!({"path": "does-not-exist.step"})),
|
||||
("file.insert_step", json!({"path": "does-not-exist.step", "up": "Z", "names": " Auto ", "folder": false})),
|
||||
]);
|
||||
}
|
||||
|
||||
fn parse(p: Value) -> Result<InsertStepParams, String> {
|
||||
serde_json::from_value(p).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The keys read as the handler reads them: the same words in any case, `null` as left out, and
|
||||
/// the keys it doesn't read in a call left unread, so a bad one there is no problem.
|
||||
#[test]
|
||||
fn the_types_read_each_key_as_the_handler_does() {
|
||||
let keys = |p: Value| parse(p).unwrap_or_else(|e| panic!("{e}")).0;
|
||||
let k = keys(json!({"path": "a.step", "name": "Mine", "up": " Y ", "names": " Solid ", "folder": true}));
|
||||
assert_eq!(k.path.as_deref(), Some("a.step"));
|
||||
assert_eq!(k.name.as_deref(), Some("Mine"));
|
||||
assert_eq!(k.up, Some(super::UpParam(true)));
|
||||
assert_eq!(k.names.map(|names| names.0), Some(solidcraft_io::StepNames::Solid));
|
||||
assert_eq!(k.folder, Some(true));
|
||||
assert_eq!(keys(json!({"path": "a.iges", "up": "Z"})).up, Some(super::UpParam(false)));
|
||||
assert_eq!(keys(json!({"path": "a.3dm", "names": " Instance "})).names.map(|names| names.0), Some(solidcraft_io::StepNames::Instance));
|
||||
assert!(keys(json!({"path": "a.step"})).folder.is_none());
|
||||
let none = keys(json!({"path": "a.step", "name": null, "up": null, "names": null, "folder": null, "matrix": null, "tolerance": null}));
|
||||
assert!(none.name.is_none() && none.up.is_none() && none.names.is_none() && none.folder.is_none() && none.matrix.is_none());
|
||||
for matrix in [json!([0, -1, 0, 100, 1, 0, 0, 0, 0, 0, 1, 0]), json!({"m": 1}), json!("turn")] {
|
||||
assert_eq!(keys(json!({"path": "a.step", "matrix": matrix.clone()})).matrix.map(|m| m.0), Some(matrix.clone()), "{matrix}");
|
||||
}
|
||||
assert_eq!(
|
||||
keys(json!({"path": "a.step", "matrix": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "tolerance": 0.001})).tolerance.map(|t| t.0),
|
||||
Some(json!(0.001))
|
||||
);
|
||||
|
||||
// Not read: `tolerance` without a `matrix`, and everything but `path` for a file the command
|
||||
// refuses before it reads another key.
|
||||
for p in [
|
||||
json!({"path": "a.step", "tolerance": "loose"}),
|
||||
json!({"path": "a.step", "matrix": null, "tolerance": []}),
|
||||
json!({"path": "a.stl", "up": "sideways", "names": 5, "folder": 5, "matrix": 5, "tolerance": 5}),
|
||||
json!({"path": "x.txt", "name": 5, "up": 1, "names": 5, "folder": 5, "matrix": 5, "tolerance": 5}),
|
||||
] {
|
||||
parse(p.clone()).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||
}
|
||||
parse(json!({"path": "A.STP", "up": "y", "names": "auto", "folder": false})).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert!(parse(json!({"path": "A.STP", "up": "y", "names": "x"})).is_err());
|
||||
let k = keys(json!({"path": "a.step", "tolerance": 5}));
|
||||
assert!(k.tolerance.is_none() && k.matrix.is_none());
|
||||
|
||||
// With `matrix` it is read as given, as `occurrence.move` reads it: the published schema
|
||||
// shapes it, and `matrix::tolerance` is the one that refuses a bad one, in its own words
|
||||
// (see `strict_keeps_todays_errors`).
|
||||
let with_matrix = json!({"path": "a.step", "matrix": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "tolerance": "loose"});
|
||||
assert_eq!(keys(with_matrix.clone()).tolerance.map(|t| t.0), Some(json!("loose")));
|
||||
assert!(crate::schema::issues("file.insert_step", &with_matrix).is_empty(), "the check leaves the refusal to the handler");
|
||||
|
||||
// Read, and refused with the handler's message.
|
||||
let refused = |p: Value| crate::schema::issues("file.insert_step", &p).into_iter().next().map(|i| i.message).unwrap_or_default();
|
||||
for (p, message) in [
|
||||
(json!({"path": "a.step", "up": "x"}), "`up` must be \"z\" (the file's axes are kept) or \"y\" (the file has Y up: its Y becomes Z)"),
|
||||
(json!({"path": "a.step", "up": "y", "names": "x"}), "`names` must be \"auto\", \"solid\", \"product\" or \"instance\""),
|
||||
(
|
||||
json!({"path": "a.step", "folder": 0}),
|
||||
"`folder` must be true (an import that brings more than one item goes into a `Group #N` history folder) or false (no folder)",
|
||||
),
|
||||
(json!({}), "`path` is required"),
|
||||
(json!({"path": " "}), "`path` is required"),
|
||||
(json!({"up": "x", "path": 5}), "`path` is required"),
|
||||
(json!({"up": "x", "names": 5, "folder": 5}), "`path` is required"),
|
||||
] {
|
||||
let e = refused(p.clone());
|
||||
assert!(e.starts_with(message), "{p}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The check names what is wrong, in the handler's words, and a key the handler would ignore is
|
||||
/// still a known key.
|
||||
#[test]
|
||||
fn the_check_names_what_is_wrong() {
|
||||
let issues = |p: Value| crate::schema::issues("file.insert_step", &p).into_iter().map(|i| i.message).collect::<Vec<_>>();
|
||||
assert_eq!(issues(json!({"path": "a.step", "toleranc": 1})), ["unknown key `toleranc`; did you mean `tolerance`?"]);
|
||||
assert_eq!(
|
||||
issues(json!({"path": "a.step", "up": "q"})),
|
||||
["`up` must be \"z\" (the file's axes are kept) or \"y\" (the file has Y up: its Y becomes Z)"]
|
||||
);
|
||||
assert_eq!(issues(json!({"path": "a.step", "name": 5})), ["`name` must be text"]);
|
||||
assert_eq!(issues(json!({})), ["`path` is required"]);
|
||||
// `name` is read as text or not at all; a number is ignored by the handler but is a mistake.
|
||||
assert_eq!(issues(json!({"path": "a.step", "name": null})), Vec::<String>::new());
|
||||
// The kind of file, and whether a matrix is a rigid move, are the handler's to refuse.
|
||||
assert!(issues(json!({"path": "a.stl", "up": "y", "names": 5, "folder": "x", "matrix": 5})).is_empty());
|
||||
assert!(issues(json!({"path": "a.step", "matrix": "turn it", "tolerance": 5})).is_empty(), "no `matrix` given means no `tolerance`");
|
||||
assert!(issues(json!({"path": "a.step", "matrix": [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0]})).is_empty());
|
||||
}
|
||||
|
||||
/// The published schema: `path` is required and shaped as the handler reads it; `tolerance` has
|
||||
/// its shape only where it is read.
|
||||
#[test]
|
||||
fn the_schema_takes_what_the_handler_reads() {
|
||||
let schema = input_schema_for::<InsertStepParams>();
|
||||
let schema = schema.as_value();
|
||||
let matrix = json!([0, -1, 0, 100, 1, 0, 0, 0, 0, 0, 1, 0]);
|
||||
for (p, takes) in [
|
||||
(json!({"path": "a.step"}), true),
|
||||
(json!({"path": "a.STP", "name": "N", "up": "y", "names": "auto", "folder": true}), true),
|
||||
(json!({"path": "a.iges", "up": null, "names": null, "folder": null, "matrix": null, "tolerance": null}), true),
|
||||
(json!({"path": "a.fcstd", "matrix": matrix, "tolerance": 1e-9}), true),
|
||||
(json!({"path": "a.3dm", "matrix": [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], "tolerance": 0.001}), true),
|
||||
// Which kinds of file the command takes is the handler's to refuse, so the schema asks
|
||||
// nothing of the extension; `tolerance` without `matrix` is the case it does describe.
|
||||
(json!({"path": "a.stl", "up": "y", "names": "auto", "folder": true}), true),
|
||||
(json!({"path": "a.step", "tolerance": "x"}), true),
|
||||
(json!({"path": "a.step", "matrix": null, "tolerance": []}), true),
|
||||
(json!({}), false),
|
||||
(json!({"path": 5}), false),
|
||||
(json!({"path": "a.step", "name": 5}), false),
|
||||
(json!({"path": "a.stl", "up": 5}), false),
|
||||
(json!({"path": "a.step", "up": 5}), false),
|
||||
(json!({"path": "a.step", "up": "q"}), false),
|
||||
(json!({"path": "a.step", "names": 5}), false),
|
||||
(json!({"path": "a.step", "up": "y", "names": "other"}), false),
|
||||
(json!({"path": "a.step", "folder": "yes"}), false),
|
||||
(json!({"path": "a.step", "folder": 1}), false),
|
||||
// A matrix is shaped as `occurrence.move` publishes it; `matrix::rigid` still decides
|
||||
// whether it is a rigid move.
|
||||
(json!({"path": "a.step", "matrix": "turn it"}), false),
|
||||
(json!({"path": "a.step", "matrix": [1, 2, 3]}), false),
|
||||
(json!({"path": "a.step", "matrix": matrix, "tolerance": "x"}), false),
|
||||
(json!({"path": "a.step", "matrix": matrix, "tolerance": 0}), false),
|
||||
(json!({"path": "a.step", "matrix": matrix, "tolerance": 0.01}), false),
|
||||
] {
|
||||
assert_eq!(conforms(schema, schema, &p), takes, "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `up`, `names`, `matrix` and `tolerance` are the shapes `doc.open` and `occurrence.move`
|
||||
/// publish, not copies of them; `path` is shaped as the handler reads it.
|
||||
#[test]
|
||||
fn the_shared_shapes_are_the_ones_published() {
|
||||
let mine = input_schema_for::<InsertStepParams>();
|
||||
let mine = mine.as_value();
|
||||
let open = input_schema_for::<crate::schema::file::OpenParams>();
|
||||
let open = open.as_value();
|
||||
let move_ = input_schema_for::<crate::schema::component::MoveParams>();
|
||||
let move_ = move_.as_value();
|
||||
for name in ["UpParam", "NamesParam"] {
|
||||
let shape = &mine["$defs"][name];
|
||||
assert!(shape.is_object(), "{name} is in my schema");
|
||||
assert_eq!(shape, &open["$defs"][name], "{name} is doc.open's shape");
|
||||
}
|
||||
for name in ["MoveMatrix", "MatrixTolerance"] {
|
||||
let shape = &mine["$defs"][name];
|
||||
assert!(shape.is_object(), "{name} is in my schema");
|
||||
assert_eq!(shape, &move_["$defs"][name], "{name} is occurrence.move's shape");
|
||||
}
|
||||
assert_eq!(mine["properties"]["path"]["pattern"], json!(crate::schema::types::not_blank_pattern()));
|
||||
assert_eq!(mine["properties"]["path"]["maxLength"], json!(4095));
|
||||
}
|
||||
|
||||
/// A fresh directory for one test's STEP files.
|
||||
fn folder(tag: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-insert-step-schema-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap_or_else(|e| panic!("{e}"));
|
||||
dir
|
||||
}
|
||||
|
||||
fn write_step(dir: &Path, name: &str, products: &[ExportProduct<'_>]) -> String {
|
||||
let text = step_export_products(products, 0, &StepHeader::default()).unwrap_or_else(|e| panic!("{e}"));
|
||||
let path = dir.join(name);
|
||||
std::fs::write(&path, text).unwrap_or_else(|e| panic!("{e}"));
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
/// A STEP file holding one solid: a box 10 mm on a side.
|
||||
fn block_file(dir: &Path) -> String {
|
||||
let block = box_solid(Vec3::ZERO, Vec3::new(10.0, 10.0, 10.0)).unwrap();
|
||||
let part = |name: &str, label: &str, body| ExportProduct {
|
||||
name: name.into(),
|
||||
bodies: vec![ExportBody { name: label.into(), body, color: None }],
|
||||
children: Vec::new(),
|
||||
};
|
||||
write_step(dir, "block.step", &[part("Block", "Block", &block)])
|
||||
}
|
||||
|
||||
/// A STEP file holding an assembly: a plate 40 wide, 4 high and 20 deep with two posts standing
|
||||
/// on it, as a Y-up file has it, so the import makes components and rigid joints of its own.
|
||||
fn frame_file(dir: &Path) -> String {
|
||||
let plate = box_solid(Vec3::ZERO, Vec3::new(40.0, 4.0, 20.0)).unwrap();
|
||||
let post = box_solid(Vec3::ZERO, Vec3::new(4.0, 30.0, 4.0)).unwrap();
|
||||
let at = |x: f64, y: f64, z: f64| [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [x, y, z, 1.0]];
|
||||
let part = |name: &str, label: &str, body| ExportProduct {
|
||||
name: name.into(),
|
||||
bodies: vec![ExportBody { name: label.into(), body, color: None }],
|
||||
children: Vec::new(),
|
||||
};
|
||||
let products = [
|
||||
ExportProduct {
|
||||
name: "Frame".into(),
|
||||
bodies: Vec::new(),
|
||||
children: vec![
|
||||
(1, at(0.0, 0.0, 0.0), "Plate-1".into()),
|
||||
(2, at(10.0, 4.0, 8.0), "Post-1".into()),
|
||||
(2, at(26.0, 4.0, 8.0), "Post-2".into()),
|
||||
],
|
||||
},
|
||||
part("Plate", "Plate", &plate),
|
||||
part("Post", "Post", &post),
|
||||
];
|
||||
write_step(dir, "frame.step", &products)
|
||||
}
|
||||
|
||||
fn matches(reply: &Value) {
|
||||
let schema = output_schema_for::<StepInserted>();
|
||||
let schema = schema.as_value();
|
||||
assert!(conforms(schema, schema, reply), "{reply} doesn't match {schema}");
|
||||
}
|
||||
|
||||
/// Every form the command takes, run in Warn: each reply matches its output schema and carries no
|
||||
/// warning (Warn runs a call the contract refuses anyway, so this shows the contract takes each
|
||||
/// call). The keys the handler leaves unread are given bad values on purpose.
|
||||
#[test]
|
||||
fn replies_match_the_output_schema() {
|
||||
let dir = folder("replies");
|
||||
let block = block_file(&dir);
|
||||
let frame = frame_file(&dir);
|
||||
let mut s = Session { param_check: ParamCheck::Warn, ..Session::default() };
|
||||
let mut run = |p: Value| {
|
||||
let r = s.execute("file.insert_step", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||
assert!(r.get("param_warnings").is_none(), "{p}: {r}");
|
||||
matches(&r);
|
||||
r
|
||||
};
|
||||
let plain = run(json!({"path": block}));
|
||||
assert_eq!(plain["component_name"], "block", "{plain}");
|
||||
assert_eq!(plain["name"], "Block", "{plain}");
|
||||
assert!(plain["bodies"] == plain["created_bodies"] && plain["occurrence"].is_u64(), "{plain}");
|
||||
assert!(plain.get("subcomponents").is_none() && plain.get("rigid_joints").is_none(), "{plain}");
|
||||
assert!(plain.get("stand_up").is_none() && plain.get("group").is_none() && plain.get("transform").is_none(), "{plain}");
|
||||
|
||||
let named = run(json!({"path": block, "name": "Brake", "up": " Z ", "names": " Solid ", "folder": false}));
|
||||
assert_eq!(named["component_name"], "Brake", "{named}");
|
||||
assert_eq!(named["name"], "Brake", "{named}");
|
||||
run(json!({"path": block, "name": null, "up": null, "names": null, "folder": null, "matrix": null, "tolerance": null}));
|
||||
// Keys left unread, with values that would be refused if they were read.
|
||||
run(json!({"path": block, "tolerance": "x"}));
|
||||
run(json!({"path": block, "matrix": null, "tolerance": []}));
|
||||
|
||||
let stood = run(json!({"path": frame}));
|
||||
assert!(stood["subcomponents"].is_u64() && stood["rigid_joints"].is_u64(), "{stood}");
|
||||
assert!(stood.get("stand_up").is_none(), "{stood}");
|
||||
let turned = run(json!({"path": frame, "up": " y "}));
|
||||
assert_eq!(turned["stand_up"], turned["occurrence"], "{turned}");
|
||||
let grouped = run(json!({"path": frame, "up": "z", "names": " Instance ", "folder": true}));
|
||||
assert!(grouped["group"].is_u64() && grouped.get("stand_up").is_none(), "{grouped}");
|
||||
let quarter = json!([0, -1, 0, 100, 1, 0, 0, 0, 0, 0, 1, 0]);
|
||||
let placed = run(json!({"path": frame, "up": "y", "matrix": quarter, "tolerance": 1e-9}));
|
||||
assert_eq!(placed["transform"][3][0], json!(100.0), "{placed}");
|
||||
|
||||
// A misspelt key still runs in Warn, says so, and the reply still matches.
|
||||
let warned = s.execute("file.insert_step", &json!({"path": block, "toleranc": 1})).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert_eq!(warned["param_warnings"], json!(["unknown key `toleranc`; did you mean `tolerance`?"]), "{warned}");
|
||||
matches(&warned);
|
||||
}
|
||||
|
||||
/// In Strict a call that works today is not refused, and one that fails fails with the handler's
|
||||
/// message (see `strict_keeps_todays_errors`).
|
||||
#[test]
|
||||
fn strict_runs_the_calls_that_work() {
|
||||
let dir = folder("strict");
|
||||
let block = block_file(&dir);
|
||||
let frame = frame_file(&dir);
|
||||
let mut s = Session { param_check: ParamCheck::Strict, ..Session::default() };
|
||||
for p in [
|
||||
json!({"path": block}),
|
||||
json!({"path": block, "name": "Brake"}),
|
||||
json!({"path": block, "up": "z", "names": "auto", "folder": true}),
|
||||
json!({"path": block, "tolerance": "x"}),
|
||||
json!({"path": block, "matrix": null, "tolerance": []}),
|
||||
json!({"path": frame, "up": "y", "names": "instance", "folder": true}),
|
||||
json!({"path": frame, "matrix": [0, -1, 0, 100, 1, 0, 0, 0, 0, 0, 1, 0], "tolerance": 1e-9}),
|
||||
] {
|
||||
s.execute("file.insert_step", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||
}
|
||||
let e = s.execute("file.insert_step", &json!({"path": block, "uints": 1})).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("unknown key `uints`"), "{e}");
|
||||
}
|
||||
@@ -74,6 +74,7 @@ pub mod features_more;
|
||||
pub mod file;
|
||||
pub mod frame;
|
||||
pub mod insert_mesh;
|
||||
pub mod insert_step;
|
||||
pub mod inspect;
|
||||
pub mod interference;
|
||||
pub mod joint_sweep;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The contract for Render Image and Check Render Request in `cmd/render/jobs.rs` (#13311). Both
|
||||
//! commands take the same image request; `render.preflight` only checks it.
|
||||
//! The contract and reply types for Render Image and Check Render Request in
|
||||
//! `cmd/render/jobs.rs` (#13311). Both commands take the same image request; `render.preflight`
|
||||
//! only checks it.
|
||||
//!
|
||||
//! The handler reads a request in one of two forms. With `progressive: true` it is the GPU form
|
||||
//! (`ProgressiveStart`); otherwise it is the runtime form (`Start`), for the raster and CPU
|
||||
@@ -20,15 +21,18 @@
|
||||
//!
|
||||
//! The render crate has no `JsonSchema`. The schema types each top-level key and publishes the
|
||||
//! render crate's option sections (`raytrace`, `settings`, `lens` and the others) as JSON objects.
|
||||
//! The replies do the same: what the engine says about the request is typed, and the render
|
||||
//! crate's sections (the request, what it resolved to, its cost and options) are objects.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::check::whole;
|
||||
use super::render_jobs::JobState;
|
||||
|
||||
/// The keys only the GPU form takes. The runtime form refuses them.
|
||||
pub(crate) const GPU_ONLY: &[&str] = &[
|
||||
@@ -346,6 +350,164 @@ impl JsonSchema for RenderRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// What Render Image (`render.start`) returns: the queued job's status, as Render Progress gives
|
||||
/// it for a job, without the shot queue.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct RenderStarted {
|
||||
#[serde(flatten)]
|
||||
pub state: JobState,
|
||||
/// For a runtime request: the setup's controls the raster image executor leaves out
|
||||
/// (`exposure_ev`, `environment_rotation`, `environment_intensity`, those not at their neutral
|
||||
/// value), as in Check Render Request. Left out for a GPU request.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ignored_controls: Option<Vec<&'static str>>,
|
||||
}
|
||||
|
||||
/// What Check Render Request (`render.preflight`) returns: the request as a job would run it, and
|
||||
/// what it would cost, without queuing anything. The reply has the request's form.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum RenderChecked {
|
||||
Runtime(RuntimeChecked),
|
||||
Gpu(GpuChecked),
|
||||
}
|
||||
|
||||
/// Check Render Request for a runtime request (raster or CPU ray tracing).
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct RuntimeChecked {
|
||||
#[serde(flatten)]
|
||||
pub checked: Checked,
|
||||
/// The setup's controls the raster image executor leaves out: `exposure_ev`,
|
||||
/// `environment_rotation` and `environment_intensity`, those not at their neutral value. Empty
|
||||
/// for CPU ray tracing, which refuses them.
|
||||
pub ignored_controls: Vec<&'static str>,
|
||||
/// How the CPU ray tracer carries light, in words: direct and mirror light only, or path
|
||||
/// lighting through a finite number of bounces, through glass too where it refracts. A raster
|
||||
/// request gets the first.
|
||||
pub stage_transport: &'static str,
|
||||
/// The CPU resource profile: `compact`, `studio` or `workstation`. Always `compact` for a
|
||||
/// raster request.
|
||||
pub cpu_budget: String,
|
||||
/// That profile's image, geometry, memory and work limits, as the render crate gives them.
|
||||
pub cpu_limits: Map<String, Value>,
|
||||
/// The bytes the request's lights, environment and resource metadata keep while the job
|
||||
/// runs, in bytes. `admission` counts them.
|
||||
pub retained_request_bytes: u64,
|
||||
}
|
||||
|
||||
/// Check Render Request for a GPU request (`progressive: true`).
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct GpuChecked {
|
||||
#[serde(flatten)]
|
||||
pub checked: Checked,
|
||||
/// The job's options, as the render crate gives them.
|
||||
pub options: Map<String, Value>,
|
||||
/// The bytes the options keep while the job runs. `admission` counts them.
|
||||
pub retained_options_bytes: u64,
|
||||
/// With `clamp_indirect`: how indirect light is clamped.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub indirect_clamp: Option<IndirectClamp>,
|
||||
}
|
||||
|
||||
/// What both forms of Check Render Request return.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct Checked {
|
||||
/// The backend and executor the request resolved to, with the image's size and format, as the
|
||||
/// render crate gives them.
|
||||
pub resolved: Map<String, Value>,
|
||||
/// The cost the job was admitted with (triangles, scratch and total bytes, and the host's
|
||||
/// memory where it has one), as the render crate gives it.
|
||||
pub admission: Map<String, Value>,
|
||||
/// The image request the job would run, as the render crate gives it.
|
||||
pub request: Map<String, Value>,
|
||||
/// The design revision the scene was captured from.
|
||||
pub scene_generation: u64,
|
||||
/// The image host that would run it: `native_worker` or `cooperative`.
|
||||
pub host: String,
|
||||
/// How many passive stage panels the setup places.
|
||||
pub stage_panels: usize,
|
||||
/// The environment's metadata, as the render crate gives it: an HDR's, or for a GPU request
|
||||
/// also a gradient sky's. `null` without one, and for a CPU sky or constant environment, which
|
||||
/// are in `request`.
|
||||
pub environment: Option<Map<String, Value>>,
|
||||
/// The surface maps the scene uses, as the render crate sums them up, or `null` without any.
|
||||
pub textures: Option<Map<String, Value>>,
|
||||
/// How transparency is rendered: `sorted_alpha_approximation` (raster), `opaque_only`,
|
||||
/// `coverage_alpha` (a transparent background) or `physical_transmission` (glass that
|
||||
/// refracts).
|
||||
pub transparency: &'static str,
|
||||
/// Whether glass in the scene refracts.
|
||||
pub refraction: bool,
|
||||
/// The glass shapes the executor refracts through: `thin_sheet`, `closed_box` and
|
||||
/// `closed_mesh`. Empty for an executor without glass, and for a runtime request without it.
|
||||
pub dielectric_boundaries: Vec<&'static str>,
|
||||
/// The setup's clear coats, as saved. Left out without any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub coats: Option<Vec<Map<String, Value>>>,
|
||||
/// With denoising on: how the image is denoised. Left out otherwise.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub denoising: Option<Denoising>,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// How a denoised image is filtered.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct Denoising {
|
||||
/// The filter: `edge_aware_atrous`.
|
||||
pub method: &'static str,
|
||||
/// For CPU ray tracing: `native_cpu`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub backend: Option<&'static str>,
|
||||
/// How many passes the filter makes, 1 to 3.
|
||||
pub passes: u32,
|
||||
/// Each pass's step between taps, in pixels: 1, 2 and 4.
|
||||
pub steps: Vec<u32>,
|
||||
/// The denoise settings, as the render crate gives them.
|
||||
pub settings: Map<String, Value>,
|
||||
/// Whether the request asked for the guide mask (`denoise_diagnostics`).
|
||||
pub diagnostics_requested: bool,
|
||||
/// What happens to isolated bright pixels, in words, or `disabled`.
|
||||
pub outlier_policy: &'static str,
|
||||
/// How mirrors and glass are filtered, in words.
|
||||
pub optical_policy: &'static str,
|
||||
/// The guide images the filter follows.
|
||||
pub guides: &'static str,
|
||||
/// What the filter leaves as rendered, in words.
|
||||
pub bypass: &'static str,
|
||||
/// What the filter can't do, in words.
|
||||
pub limitation: &'static str,
|
||||
/// Always true: the unfiltered linear image is kept.
|
||||
pub raw_linear_accumulator_preserved: bool,
|
||||
/// For CPU ray tracing, always true: a kept linear EXR is unfiltered.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub raw_linear_exr_preserved: Option<bool>,
|
||||
/// Where in the pipeline the filter runs: `linear_before_tone_map`.
|
||||
pub output_stage: &'static str,
|
||||
/// For CPU ray tracing: the filter's work, in words.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub work_units: Option<&'static str>,
|
||||
/// For CPU ray tracing: which checkpoint images are filtered, in words.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub checkpoints: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// How a GPU request's indirect light is clamped (`clamp_indirect`).
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct IndirectClamp {
|
||||
/// The largest contribution a path keeps.
|
||||
pub limit: f64,
|
||||
/// The bounce the clamp starts from: 1, the first indirect one.
|
||||
pub from_bounce: u32,
|
||||
/// Always true: a clamped contribution keeps its hue.
|
||||
pub keeps_hue: bool,
|
||||
/// Always true: the clamp darkens real caustics and glints.
|
||||
pub biased: bool,
|
||||
/// What the clamp changes beyond the image, in words.
|
||||
pub limitation: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "render_start_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_render::job::CpuHost;
|
||||
use solidcraft_render::job::{Admission, CancelToken, CpuHost, ImageHost, ImageRequest, ImageTask, ImageWork, JobId, JobOptions};
|
||||
|
||||
use super::{GPU_ONLY, RUNTIME_ONLY, RenderRequest};
|
||||
use super::{GPU_ONLY, RUNTIME_ONLY, RenderChecked, RenderRequest, RenderStarted};
|
||||
use crate::Session;
|
||||
use crate::schema::check::schema_keys;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
use crate::schema::{ParamCheck, input_schema_for};
|
||||
use crate::schema::{ParamCheck, input_schema_for, output_schema_for};
|
||||
|
||||
const IDS: [&str; 2] = ["render.start", "render.preflight"];
|
||||
|
||||
@@ -22,6 +22,16 @@ fn both_commands_describe_their_parameters() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #13311: `engine.describe` gives Render Image and Check Render Request their replies too.
|
||||
#[test]
|
||||
fn both_commands_describe_their_replies() {
|
||||
let mut s = Session::default();
|
||||
for id in IDS {
|
||||
let described = s.execute("engine.describe", &json!({"command": id})).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert!(described["output"].is_object(), "{id}: {described}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The fields of the handler's reader for one form, from serde's message for an unknown key,
|
||||
/// which lists them all.
|
||||
fn handler_fields(progressive: bool) -> BTreeSet<String> {
|
||||
@@ -285,6 +295,178 @@ fn the_check_takes_what_the_schema_allows() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A GPU executor without a GPU, as in `cmd/render/job_tests.rs`: the cooperative CPU host's
|
||||
/// executor, named `gpu-path` and taking progressive requests.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct NumericalGpu;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl solidcraft_render::job::ImageExecutor for NumericalGpu {
|
||||
fn descriptor(&self) -> solidcraft_render::job::Result<solidcraft_render::ExecutorDescriptor> {
|
||||
let mut descriptor = CpuHost::cooperative().descriptor()?;
|
||||
descriptor.id = solidcraft_render::ExecutorId::new("gpu-path").unwrap_or_else(|e| panic!("{e}"));
|
||||
descriptor.profiles[0].features.extend([solidcraft_render::RenderFeature::Progressive, solidcraft_render::RenderFeature::Hdri]);
|
||||
Ok(descriptor)
|
||||
}
|
||||
fn admit(
|
||||
&self,
|
||||
scene: &solidcraft_render::ProductScene,
|
||||
request: &ImageRequest,
|
||||
options: JobOptions,
|
||||
) -> solidcraft_render::job::Result<Admission> {
|
||||
options.validate()?;
|
||||
solidcraft_render::job::admit(scene, &ImageRequest { environment: None, textures: None, ..request.clone() })
|
||||
}
|
||||
fn begin(
|
||||
&self,
|
||||
scene: solidcraft_render::ProductScene,
|
||||
request: ImageRequest,
|
||||
_: JobOptions,
|
||||
cancel: CancelToken,
|
||||
) -> solidcraft_render::job::Result<Box<dyn ImageWork>> {
|
||||
Ok(Box::new(ImageTask::new(scene, ImageRequest { environment: None, textures: None, ..request }, cancel)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// The keys of Check Render Request's reply in either form, then only in the runtime or the GPU
|
||||
/// form, and of its `denoising` in either form (CPU ray tracing adds four), as the handler gave
|
||||
/// them before its reply had a type.
|
||||
const SHARED_KEYS: &[&str] = &[
|
||||
"admission",
|
||||
"dielectric_boundaries",
|
||||
"environment",
|
||||
"host",
|
||||
"refraction",
|
||||
"request",
|
||||
"resolved",
|
||||
"scene_generation",
|
||||
"stage_panels",
|
||||
"textures",
|
||||
"transparency",
|
||||
];
|
||||
const RUNTIME_KEYS: &[&str] = &["cpu_budget", "cpu_limits", "ignored_controls", "retained_request_bytes", "stage_transport"];
|
||||
const GPU_KEYS: &[&str] = &["options", "retained_options_bytes"];
|
||||
const DENOISING_KEYS: &[&str] = &[
|
||||
"bypass",
|
||||
"diagnostics_requested",
|
||||
"guides",
|
||||
"limitation",
|
||||
"method",
|
||||
"optical_policy",
|
||||
"outlier_policy",
|
||||
"output_stage",
|
||||
"passes",
|
||||
"raw_linear_accumulator_preserved",
|
||||
"settings",
|
||||
"steps",
|
||||
];
|
||||
|
||||
/// An object's keys, sorted.
|
||||
fn keys(v: &Value) -> Vec<String> {
|
||||
v.as_object().map(|o| o.keys().cloned().collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `keys`, sorted.
|
||||
fn sorted(keys: &[&str]) -> Vec<String> {
|
||||
let mut keys: Vec<String> = keys.iter().map(|k| k.to_string()).collect();
|
||||
keys.sort();
|
||||
keys
|
||||
}
|
||||
|
||||
/// `params` run as `id` in Warn: the reply, which must match `schema` and have no warnings.
|
||||
fn reply(s: &mut Session, id: &str, params: &Value, schema: &schemars::Schema) -> Value {
|
||||
let r = s.execute(id, params).unwrap_or_else(|e| panic!("{id} {params}: {e}"));
|
||||
assert!(conforms(schema.as_value(), schema.as_value(), &r), "{id} {params}: {r} doesn't match {}", schema.as_value());
|
||||
assert!(r.get("param_warnings").is_none(), "{id} {params}: {r}");
|
||||
r
|
||||
}
|
||||
|
||||
/// #13311: both commands reply as their output schemas say, in Warn without warnings, in either
|
||||
/// form: a raster request, one whose setup has a control the raster executor leaves out, and GPU
|
||||
/// requests with and without denoising and the indirect clamp. Render Image's reply is the queued
|
||||
/// job's status, with the ignored controls of a runtime request; Check Render Request's has the
|
||||
/// sections of its form.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn replies_match_the_output_schemas() {
|
||||
let (checked, started) = (output_schema_for::<RenderChecked>(), output_schema_for::<RenderStarted>());
|
||||
let small = json!({"width": 16, "height": 16});
|
||||
for (p, ignored) in [
|
||||
(json!({"settings": small}), Some(json!([]))),
|
||||
(json!({"setup": "Bright", "transparent_background": true}), Some(json!(["exposure_ev"]))),
|
||||
(json!({"settings": small, "progressive": true}), None),
|
||||
(json!({"settings": small, "progressive": true, "denoise": true, "clamp_indirect": 8}), None),
|
||||
] {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 10, "height": 5})).unwrap_or_else(|e| panic!("{e}"));
|
||||
let bright = json!({"name": "Bright", "set": {"width": 16, "height": 16, "exposure_ev": 1}});
|
||||
s.execute("render.settings.set", &bright).unwrap_or_else(|e| panic!("{e}"));
|
||||
s.set_render_job_host(Some(CpuHost::cooperative())).unwrap_or_else(|e| panic!("{e}"));
|
||||
let gpu = ImageHost::with_executor(std::sync::Arc::new(NumericalGpu)).unwrap_or_else(|e| panic!("{e}"));
|
||||
s.register_render_job_host(gpu).unwrap_or_else(|e| panic!("{e}"));
|
||||
s.param_check = ParamCheck::Warn;
|
||||
|
||||
let r = reply(&mut s, "render.preflight", &p, &checked);
|
||||
assert_eq!(r.get("ignored_controls"), ignored.as_ref(), "{p}: {r}");
|
||||
let gpu = p["progressive"] == true;
|
||||
let mut expected: Vec<&str> = SHARED_KEYS.iter().chain(if gpu { GPU_KEYS } else { RUNTIME_KEYS }).copied().collect();
|
||||
if p["denoise"] == true {
|
||||
expected.extend(["denoising", "indirect_clamp"]);
|
||||
assert_eq!(keys(&r["denoising"]), sorted(DENOISING_KEYS), "{r}");
|
||||
assert_eq!(keys(&r["indirect_clamp"]), sorted(&["biased", "from_bounce", "keeps_hue", "limit", "limitation"]), "{r}");
|
||||
}
|
||||
assert_eq!(keys(&r), sorted(&expected), "{p}");
|
||||
assert_eq!(r["resolved"]["executor"], if gpu { "gpu-path" } else { "cpu-product" }, "{p}: {r}");
|
||||
|
||||
let job = reply(&mut s, "render.start", &p, &started);
|
||||
assert_eq!(job.get("ignored_controls"), ignored.as_ref(), "{p}: {job}");
|
||||
if !gpu {
|
||||
// The cooperative host doesn't run a job until it is pumped, so its status is as queued.
|
||||
let id: JobId = serde_json::from_value(job["job"].clone()).unwrap_or_else(|e| panic!("{e}"));
|
||||
let mut status = json!(s.render_jobs.status(id).unwrap_or_else(|e| panic!("{e}")));
|
||||
status["ignored_controls"] = ignored.clone().unwrap_or_default();
|
||||
assert_eq!(job, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #13311: CPU ray tracing replies as the output schemas say too, with the sections only it has:
|
||||
/// the CPU filter's denoising and a setup's clear coats.
|
||||
#[cfg(all(feature = "raytrace-cpu", not(target_arch = "wasm32")))]
|
||||
#[test]
|
||||
fn cpu_ray_tracing_replies_match_the_output_schemas() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"body_name": "Panel", "length": 20, "width": 20, "height": 2})).unwrap_or_else(|e| panic!("{e}"));
|
||||
let options =
|
||||
json!({"progressive": {"samples_per_pixel": 1, "seed": 7}, "lights": [{"direction": [0, 0, 1], "color": [1, 1, 1], "intensity": 1}]});
|
||||
let mut denoised = options.clone();
|
||||
denoised["denoise"] = json!(true);
|
||||
let set = |options: &Value| json!({"width": 8, "height": 8, "job": {"method": "cpu_raytrace", "options": options}});
|
||||
s.execute("render.settings.set", &json!({"name": "Denoised", "set": set(&denoised)})).unwrap_or_else(|e| panic!("{e}"));
|
||||
let coat = json!({"name": "Coat", "body_index": 0, "body_name": "Panel", "expected_revision": s.revision,
|
||||
"coat": {"weight": 0.5, "roughness": 0.5, "ior": 1.5}, "set": set(&options)});
|
||||
s.execute("render.coat.bind", &coat).unwrap_or_else(|e| panic!("{e}"));
|
||||
s.set_render_job_host(Some(CpuHost::for_platform().unwrap_or_else(|e| panic!("{e}")))).unwrap_or_else(|e| panic!("{e}"));
|
||||
s.param_check = ParamCheck::Warn;
|
||||
let (checked, started) = (output_schema_for::<RenderChecked>(), output_schema_for::<RenderStarted>());
|
||||
for (setup, section) in [("Denoised", "denoising"), ("Coat", "coats")] {
|
||||
let p = json!({"setup": setup, "replace": true});
|
||||
let r = reply(&mut s, "render.preflight", &p, &checked);
|
||||
assert_eq!(r["resolved"]["executor"], "cpu-raytrace", "{r}");
|
||||
let mut expected: Vec<&str> = SHARED_KEYS.iter().chain(RUNTIME_KEYS).copied().collect();
|
||||
expected.push(section);
|
||||
assert_eq!(keys(&r), sorted(&expected), "{setup}");
|
||||
if section == "denoising" {
|
||||
let cpu_only = ["backend", "checkpoints", "raw_linear_exr_preserved", "work_units"];
|
||||
assert_eq!(keys(&r["denoising"]), sorted(&[DENOISING_KEYS, &cpu_only].concat()), "{r}");
|
||||
assert_eq!(r["denoising"]["backend"], "native_cpu", "{r}");
|
||||
assert_eq!(r["denoising"]["guides"], "deterministic_center_ray_albedo_normal_camera_relative_position_depth", "{r}");
|
||||
}
|
||||
let job = reply(&mut s, "render.start", &p, &started);
|
||||
assert_eq!(job["ignored_controls"], json!([]), "{job}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #13311: in Warn, a request the contract takes runs as it did with the check off and gets no
|
||||
/// warnings: Check Render Request's reply is the same, and Render Image queues a job. Among them
|
||||
/// a saved setup with a job and an inline one, which the handler lowers itself.
|
||||
|
||||
@@ -277,6 +277,28 @@ fn hidden<'de, D: Deserializer<'de>>(d: D) -> Result<Option<HiddenLines>, D::Err
|
||||
read_as(d, "hidden must be omit or dashed")
|
||||
}
|
||||
|
||||
/// How the drawing's curved edges are written.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DxfCurves {
|
||||
/// As short straight lines, as the view is tessellated.
|
||||
Lines,
|
||||
/// Round holes and arcs as `CIRCLE` and `ARC` entities.
|
||||
Arcs,
|
||||
}
|
||||
|
||||
fn curves_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"description": "How curved edges are written: `lines` writes the short straight lines the view is built from; `arcs` writes the visible circles and arcs as CIRCLE and ARC entities, which a milling or CAD program can pick as circles. Default: lines.",
|
||||
"enum": ["lines", "arcs"],
|
||||
"default": "lines"
|
||||
})
|
||||
}
|
||||
|
||||
fn curves<'de, D: Deserializer<'de>>(d: D) -> Result<Option<DxfCurves>, D::Error> {
|
||||
read_as(d, "curves must be lines or arcs")
|
||||
}
|
||||
|
||||
/// Where the drawing's zero point is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -355,6 +377,11 @@ pub struct ExportViewDxfParams {
|
||||
#[serde(default, deserialize_with = "zero", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "zero_schema")]
|
||||
pub zero: Option<DxfZero>,
|
||||
/// How curved edges are written: as short lines or, for visible circles and arcs, as CIRCLE
|
||||
/// and ARC. Default: lines.
|
||||
#[serde(default, deserialize_with = "curves", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "curves_schema")]
|
||||
pub curves: Option<DxfCurves>,
|
||||
/// The `.dxf` file to write. Default: the reply carries the DXF text instead.
|
||||
#[serde(default, deserialize_with = "export_path", skip_serializing_if = "Option::is_none")]
|
||||
#[schemars(schema_with = "path_schema")]
|
||||
|
||||
@@ -29,7 +29,7 @@ fn matches(schema: schemars::Schema, id: &str, reply: &Value) {
|
||||
#[test]
|
||||
fn schemas_and_the_key_check_agree() {
|
||||
assert_keys(input_schema_for::<SilhouetteParams>(), &["bodies", "camera", "rows", "view"]);
|
||||
assert_keys(input_schema_for::<ExportViewDxfParams>(), &["camera", "hidden", "path", "tolerance", "zero"]);
|
||||
assert_keys(input_schema_for::<ExportViewDxfParams>(), &["camera", "curves", "hidden", "path", "tolerance", "zero"]);
|
||||
}
|
||||
|
||||
/// Strict gives the handlers' messages for the calls they refuse for a key (a missing `camera`
|
||||
@@ -211,6 +211,8 @@ fn the_schemas_take_what_the_handlers_read() {
|
||||
(json!({"camera": camera(), "pathh": "a.dxf"}), false),
|
||||
(json!({"camera": camera(), "zero": "origin"}), true),
|
||||
(json!({"camera": camera(), "zero": "corner"}), false),
|
||||
(json!({"camera": camera(), "curves": "arcs"}), true),
|
||||
(json!({"camera": camera(), "curves": "splines"}), false),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -226,6 +228,7 @@ fn the_check_names_what_is_wrong_with_a_key() {
|
||||
assert_eq!(issues(DXF, json!({"camera": camera(), "hidden": 1})), ["hidden must be omit or dashed"]);
|
||||
assert_eq!(issues(DXF, json!({"camera": camera(), "tolerance": 3})), ["tolerance must be 0.0001..1 mm"]);
|
||||
assert_eq!(issues(DXF, json!({"camera": camera(), "zero": "corner"})), ["zero must be target or origin"]);
|
||||
assert_eq!(issues(DXF, json!({"camera": camera(), "curves": "splines"})), ["curves must be lines or arcs"]);
|
||||
assert_eq!(issues(DXF, json!({"camera": camera(), "path": " "})), ["invalid path"]);
|
||||
assert_eq!(issues(DXF, json!({"camera": 1})), ["invalid camera"]);
|
||||
// Left out, the handler's own refusal stands in for "is required".
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
# type, remove its line. Never add one: a new command comes with both schemas. Sorted, one id
|
||||
# per line.
|
||||
face.delete
|
||||
file.insert_step
|
||||
render.aging.describe
|
||||
render.backends
|
||||
render.effects.describe
|
||||
render.preflight
|
||||
render.scene
|
||||
render.start
|
||||
sheet.export_dxf
|
||||
solid.draft
|
||||
solid.hole
|
||||
|
||||
@@ -117,8 +117,6 @@ pub(crate) fn fixes() -> UpstreamFixes {
|
||||
f.fillet_nx1_end_rib_global_merge = true;
|
||||
f.uv_trim_corner_end_points_only = true;
|
||||
f.bspline_trim_preserves_uv_spans = true;
|
||||
f.cone_parallel_line_midpoint = true;
|
||||
f.cone_parallel_line_domains = true;
|
||||
f.proto_edge_self_loop_on_vertex_degenerate = true;
|
||||
f.slice_coincident_faces_sheet_side = true;
|
||||
f.boolean_outer_shell_first = true;
|
||||
|
||||
+298
-110
@@ -1,9 +1,11 @@
|
||||
//! The Bill of Materials window: `assembly.bom`'s rows (flat or top level, grouped by part
|
||||
//! The Bill of Materials panel: `assembly.bom`'s rows (flat or top level, grouped by part
|
||||
//! number or by definition, a part's sub-parts in its row or not), with Export CSV… and Export
|
||||
//! JSON…. Picking a row edits its part
|
||||
//! number, description and revision (`metadata::BOM_KEYS`) through one `component.metadata.set`
|
||||
//! per definition. The toolbar's Bill of Materials (`ui.start` of `assembly.bom`) opens it.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use egui::RichText;
|
||||
use serde_json::{Map, Value, json};
|
||||
use solidcraft_engine::doc::metadata::{self, BOM_KEYS};
|
||||
@@ -11,9 +13,11 @@ use solidcraft_engine::doc::metadata::{self, BOM_KEYS};
|
||||
use crate::SolveApp;
|
||||
use solidcraft_theme::Tokens;
|
||||
|
||||
/// The window's state; the rows come from the engine.
|
||||
/// The panel's state; the rows come from the engine.
|
||||
#[derive(Default)]
|
||||
pub struct BomPanel {
|
||||
document: String,
|
||||
focus_pending: bool,
|
||||
/// Top level instead of flat.
|
||||
pub top: bool,
|
||||
/// One row per definition instead of per part number.
|
||||
@@ -21,10 +25,12 @@ pub struct BomPanel {
|
||||
/// A part with a part number keeps its sub-parts (a bolt's washers and nut) in its row.
|
||||
pub group_children: bool,
|
||||
/// The rows for (design revision, top, by_definition, group_children).
|
||||
cache: Option<(u64, [bool; 3], Value)>,
|
||||
cache: Option<(u64, [bool; 3], Rc<Value>)>,
|
||||
/// The picked row's index, and its fields as edited (one for each of [`BOM_KEYS`]).
|
||||
pub picked: Option<usize>,
|
||||
pub fields: Vec<String>,
|
||||
/// The selected definitions, whose row index can change when the table refreshes.
|
||||
target: Option<(Value, Value)>,
|
||||
/// The last refusal.
|
||||
pub error: Option<String>,
|
||||
/// The design revision, the picked row and its fields when Apply was refused: `error` stays
|
||||
@@ -33,6 +39,24 @@ pub struct BomPanel {
|
||||
}
|
||||
|
||||
impl BomPanel {
|
||||
fn clear_pick(&mut self) {
|
||||
self.picked = None;
|
||||
self.target = None;
|
||||
self.fields.clear();
|
||||
}
|
||||
|
||||
fn reconcile_pick(&mut self, rows: &[Value]) {
|
||||
let matches_target = |row: &Value| self.target.as_ref().is_some_and(|(ids, names)| &row["components"] == ids && &row["definition"] == names);
|
||||
if let Some(index) = self.picked
|
||||
&& !rows.get(index).is_some_and(matches_target)
|
||||
{
|
||||
self.picked = rows.iter().position(matches_target);
|
||||
if self.picked.is_none() {
|
||||
self.clear_pick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show why Apply was refused, for as long as the design, the row and its fields stay as they are.
|
||||
fn refuse(&mut self, revision: u64, error: String) {
|
||||
self.refused = Some((revision, self.picked, self.fields.clone()));
|
||||
@@ -45,7 +69,7 @@ impl BomPanel {
|
||||
}
|
||||
|
||||
/// The bill of materials for the design as it is now (computed again only after a change).
|
||||
fn rows(&mut self, app: &mut SolveApp) -> Value {
|
||||
fn rows(&mut self, app: &mut SolveApp) -> Rc<Value> {
|
||||
let revision = app.session.revision;
|
||||
let key = [self.top, self.by_definition, self.group_children];
|
||||
if let Some((r, k, v)) = &self.cache
|
||||
@@ -60,14 +84,33 @@ impl BomPanel {
|
||||
json!({"rows": [], "total_occurrences": 0})
|
||||
}
|
||||
};
|
||||
let v = Rc::new(v);
|
||||
self.cache = Some((revision, key, v.clone()));
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the window.
|
||||
pub(crate) const TITLE_KEY: &str = "cmd.assembly_bom";
|
||||
|
||||
/// Open the report for the current document.
|
||||
pub fn open(app: &mut SolveApp) {
|
||||
app.bom = Some(BomPanel::default());
|
||||
app.bom = Some(BomPanel { document: app.session.document_id.clone(), focus_pending: true, ..BomPanel::default() });
|
||||
}
|
||||
|
||||
pub(crate) fn available(app: &SolveApp) -> bool {
|
||||
app.bom.as_ref().is_some_and(|d| d.document == app.session.document_id)
|
||||
}
|
||||
|
||||
pub(crate) fn focus_pending(app: &SolveApp) -> bool {
|
||||
app.bom.as_ref().is_some_and(|d| d.document == app.session.document_id && d.focus_pending)
|
||||
}
|
||||
|
||||
pub(crate) fn set_focus_pending(app: &mut SolveApp, pending: bool) {
|
||||
if let Some(d) = &mut app.bom
|
||||
&& d.document == app.session.document_id
|
||||
{
|
||||
d.focus_pending = pending;
|
||||
}
|
||||
}
|
||||
|
||||
fn text(row: &Value, k: &str) -> String {
|
||||
@@ -86,7 +129,7 @@ const DESCRIPTION: usize = 2;
|
||||
const COLUMN_SPACING: f32 = 14.0;
|
||||
|
||||
/// Each column's width: every column fits its widest cell, Description takes what is left.
|
||||
fn column_widths(ui: &egui::Ui, rows: &[Value]) -> [f32; 6] {
|
||||
fn column_widths(ui: &egui::Ui, rows: &[Value], scrollable: bool) -> [f32; 6] {
|
||||
let font = egui::TextStyle::Body.resolve(ui.style());
|
||||
let width = |s: &str| ui.painter().layout_no_wrap(s.to_owned(), font.clone(), egui::Color32::PLACEHOLDER).size().x.ceil();
|
||||
// The item cell is a picker button, padded on both sides.
|
||||
@@ -94,13 +137,16 @@ fn column_widths(ui: &egui::Ui, rows: &[Value]) -> [f32; 6] {
|
||||
let mut widths = HEADERS.map(width);
|
||||
for row in rows.iter().flat_map(|r| std::iter::once(r).chain(r["children"].as_array().into_iter().flatten())) {
|
||||
for (i, key) in COLUMN_KEYS.iter().enumerate() {
|
||||
if i == DESCRIPTION && !scrollable {
|
||||
continue;
|
||||
}
|
||||
let w = width(&text(row, key)) + if i == 0 { padding } else { 0.0 };
|
||||
widths[i] = widths[i].max(w);
|
||||
}
|
||||
}
|
||||
let others: f32 = widths.iter().enumerate().filter(|(i, _)| *i != DESCRIPTION).map(|(_, w)| w).sum();
|
||||
let left = ui.available_width() - others - COLUMN_SPACING * (HEADERS.len() - 1) as f32;
|
||||
widths[DESCRIPTION] = left.max(width(HEADERS[DESCRIPTION]));
|
||||
widths[DESCRIPTION] = left.max(widths[DESCRIPTION]);
|
||||
widths
|
||||
}
|
||||
|
||||
@@ -133,118 +179,260 @@ fn apply(app: &mut SolveApp, d: &mut BomPanel, row: &Value) {
|
||||
d.refused = None;
|
||||
}
|
||||
|
||||
/// Draw the window when it is open. Called once per frame.
|
||||
fn export(app: &mut SolveApp, d: &BomPanel, ext: &str) {
|
||||
let mut params = d.params();
|
||||
params["format"] = json!(ext);
|
||||
let document = d.document.clone();
|
||||
app.pick_save(&format!("bill-of-materials.{ext}"), &[ext], move |app, path| {
|
||||
if let Some(path) = path
|
||||
&& app.session.document_id == document
|
||||
{
|
||||
params["path"] = json!(path);
|
||||
let _ = app.run("assembly.bom", params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw the Classic report window when it is open.
|
||||
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let Some(mut d) = app.bom.take() else { return };
|
||||
// A refused Apply's message is about the design, row and fields it ran with (#641): once they
|
||||
// change, it goes.
|
||||
let Some(mut d) = take_current(app) else { return };
|
||||
let closed = crate::ui_kit::Window::new(egui::Id::unique("sc_bom"), "bom", solidcraft_ui_text::tr(TITLE_KEY))
|
||||
.cards(3.0)
|
||||
.show_body(ctx, |ui| body(app, ui, &mut d, false));
|
||||
if !closed {
|
||||
app.bom = Some(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the same report inside the shared panel's body.
|
||||
pub(crate) fn shared_content(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let Some(mut d) = take_current(app) else { return };
|
||||
body(app, ui, &mut d, true);
|
||||
app.bom = Some(d);
|
||||
}
|
||||
|
||||
fn take_current(app: &mut SolveApp) -> Option<BomPanel> {
|
||||
app.bom.take().filter(|d| d.document == app.session.document_id)
|
||||
}
|
||||
|
||||
const REPORT_MAX_HEIGHT: f32 = 320.0;
|
||||
const EDITOR_MAX_HEIGHT: f32 = 200.0;
|
||||
|
||||
fn body(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut BomPanel, shared: bool) {
|
||||
// A refused Apply belongs to the design, row and fields it ran with (#641).
|
||||
if d.refused.as_ref().is_some_and(|(rev, row, fields)| (*rev, *row, fields) != (app.session.revision, d.picked, &d.fields)) {
|
||||
d.error = None;
|
||||
d.refused = None;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
let bom = d.rows(app);
|
||||
let rows: Vec<Value> = bom["rows"].as_array().cloned().unwrap_or_default();
|
||||
let (mut export, mut apply_row) = (None, None);
|
||||
let closed = crate::ui_kit::Window::new(egui::Id::unique("sc_bom"), "bom", solidcraft_ui_text::tr_label("Bill of Materials"))
|
||||
.cards(3.0)
|
||||
.show_body(ctx, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
let was = (d.top, d.by_definition, d.group_children);
|
||||
let mut levels = usize::from(d.top);
|
||||
let levels_width =
|
||||
crate::ui_kit::ContentWidth::button(ui, "Parts only").max(crate::ui_kit::ContentWidth::button(ui, "Top level")) * 2.0;
|
||||
ui.allocate_ui(egui::vec2(levels_width, crate::ui_kit::row_height()), |ui| {
|
||||
crate::ui_kit::segmented(ui, &mut levels, &["Parts only", "Top level"], Default::default());
|
||||
});
|
||||
d.top = levels == 1;
|
||||
ui.separator();
|
||||
let mut grouping = usize::from(d.by_definition);
|
||||
let grouping_width =
|
||||
crate::ui_kit::ContentWidth::button(ui, "By part number").max(crate::ui_kit::ContentWidth::button(ui, "By definition")) * 2.0;
|
||||
ui.allocate_ui(egui::vec2(grouping_width, crate::ui_kit::row_height()), |ui| {
|
||||
crate::ui_kit::segmented(ui, &mut grouping, &["By part number", "By definition"], Default::default());
|
||||
});
|
||||
d.by_definition = grouping == 1;
|
||||
ui.separator();
|
||||
crate::ui_kit::checkbox(ui, solidcraft_ui_text::tr("bom.group_children"), &mut d.group_children);
|
||||
if was != (d.top, d.by_definition, d.group_children) {
|
||||
d.picked = None;
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
let widths = column_widths(ui, &rows);
|
||||
// The item cell (a picker for a row, dim text for a sub-part) is drawn by the caller.
|
||||
let rest = |ui: &mut egui::Ui, row: &Value| {
|
||||
for (i, key) in COLUMN_KEYS.iter().enumerate().skip(1) {
|
||||
let dim = *key == "definition";
|
||||
cell(ui, widths[i], |ui| {
|
||||
ui.label(if dim { RichText::new(text(row, key)).color(t.text_dim) } else { RichText::new(text(row, key)) })
|
||||
});
|
||||
}
|
||||
ui.end_row();
|
||||
};
|
||||
// No minimum column width: the widths above add up to the grid's, so padding a
|
||||
// narrow column (Rev, Qty) would push the grid out of the window.
|
||||
egui::Grid::new("sc_bom_grid").num_columns(6).striped(true).min_col_width(0.0).spacing([COLUMN_SPACING, 4.0]).show(ui, |ui| {
|
||||
for (h, w) in HEADERS.iter().zip(widths) {
|
||||
cell(ui, w, |ui| ui.label(RichText::new(*h).strong().color(t.text_dim)));
|
||||
}
|
||||
ui.end_row();
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let picked = d.picked == Some(i);
|
||||
if cell(ui, widths[0], |ui| crate::ui_kit::list_picker(ui, &i.to_string(), &text(row, "item"), picked)).clicked() {
|
||||
d.picked = if picked { None } else { Some(i) };
|
||||
d.fields = BOM_KEYS.iter().map(|k| text(row, k)).collect();
|
||||
}
|
||||
rest(ui, row);
|
||||
for c in row["children"].as_array().into_iter().flatten() {
|
||||
cell(ui, widths[0], |ui| ui.label(RichText::new(text(c, "item")).color(t.text_dim)));
|
||||
rest(ui, c);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.label(RichText::new(format!("{} rows, {} placed", rows.len(), text(&bom, "total_occurrences"))).small().color(t.text_dim));
|
||||
if let Some(row) = d.picked.and_then(|i| rows.get(i)) {
|
||||
ui.separator();
|
||||
egui::Grid::new("sc_bom_edit").num_columns(2).spacing([12.0, 6.0]).show(ui, |ui| {
|
||||
for (key, field) in BOM_KEYS.iter().zip(d.fields.iter_mut()) {
|
||||
let label = field_label(key);
|
||||
ui.label(label);
|
||||
ui.allocate_ui(egui::vec2(280.0, crate::ui_kit::row_height()), |ui| {
|
||||
crate::ui_kit::cell_editor(ui, ui.make_persistent_id(key), field, label, false);
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
if crate::ui_kit::button(ui, "Apply", crate::ui_kit::Button::Normal).clicked() {
|
||||
apply_row = Some(row.clone());
|
||||
}
|
||||
let top = ui.cursor().top();
|
||||
let (footer_action, footer_height) = if shared {
|
||||
let selected = d.picked.is_some();
|
||||
let rows = if selected && ui.available_width() < 290.0 { 2 } else { 1 };
|
||||
let footer = crate::ui_kit::Panel::pinned_footer(ui, "bom-actions", rows, |ui| actions(ui, selected, true));
|
||||
(footer.inner, footer.response.rect.height())
|
||||
} else {
|
||||
(None, 0.0)
|
||||
};
|
||||
let available = ui.available_height().max(1.0);
|
||||
let (row, report_extent) = if shared {
|
||||
let max = if d.picked.is_some() || d.error.is_some() { available * 0.55 } else { available };
|
||||
let scroll = egui::ScrollArea::vertical()
|
||||
.id_salt("report")
|
||||
.min_scrolled_height(0.0)
|
||||
.max_height(max.min(REPORT_MAX_HEIGHT))
|
||||
.show(ui, |ui| report(app, ui, d, true));
|
||||
let (row, table_extra) = scroll.inner;
|
||||
(row, (scroll.content_size.y + table_extra).min(REPORT_MAX_HEIGHT))
|
||||
} else {
|
||||
report(app, ui, d, false)
|
||||
};
|
||||
|
||||
let mut editor_extent = 0.0;
|
||||
if row.is_some() || d.error.is_some() {
|
||||
ui.separator();
|
||||
if shared {
|
||||
let max = ui.available_height().clamp(1.0, EDITOR_MAX_HEIGHT);
|
||||
let scroll = egui::ScrollArea::vertical()
|
||||
.id_salt("editor")
|
||||
.min_scrolled_height(0.0)
|
||||
.max_height(max)
|
||||
.show(ui, |ui| details(ui, d, row.is_some(), true));
|
||||
editor_extent = scroll.content_size.y.min(EDITOR_MAX_HEIGHT) + 2.0 * ui.spacing().item_spacing.y;
|
||||
} else {
|
||||
details(ui, d, row.is_some(), false);
|
||||
}
|
||||
}
|
||||
|
||||
let action = if shared {
|
||||
footer_action
|
||||
} else {
|
||||
ui.separator();
|
||||
actions(ui, row.is_some(), false)
|
||||
};
|
||||
match action {
|
||||
Some(Action::Apply) => {
|
||||
if let Some(row) = &row {
|
||||
apply(app, d, row);
|
||||
}
|
||||
if let Some(e) = &d.error {
|
||||
ui.label(RichText::new(e).color(t.error));
|
||||
}
|
||||
Some(Action::Export(ext)) => export(app, d, ext),
|
||||
None => {}
|
||||
}
|
||||
if shared {
|
||||
let natural = report_extent + editor_extent + footer_height + ui.spacing().item_spacing.y;
|
||||
crate::panel_runtime::report_content_extent(ui, top, natural, false);
|
||||
}
|
||||
}
|
||||
|
||||
enum Action {
|
||||
Apply,
|
||||
Export(&'static str),
|
||||
}
|
||||
|
||||
fn actions(ui: &mut egui::Ui, selected: bool, shared: bool) -> Option<Action> {
|
||||
let buttons = |ui: &mut egui::Ui| {
|
||||
let mut action = None;
|
||||
if selected {
|
||||
let response = crate::ui_kit::button(ui, solidcraft_ui_text::tr_label("Apply"), crate::ui_kit::Button::Normal);
|
||||
crate::panel_runtime::register_content_focus(ui.ctx(), &response);
|
||||
if response.clicked() {
|
||||
action = Some(Action::Apply);
|
||||
}
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
for (label, ext) in [("Export CSV…", "csv"), ("Export JSON…", "json")] {
|
||||
if crate::ui_kit::button(ui, label, crate::ui_kit::Button::Normal).clicked() {
|
||||
export = Some(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (label, ext) in [("Export CSV…", "csv"), ("Export JSON…", "json")] {
|
||||
let response = crate::ui_kit::button(ui, solidcraft_ui_text::tr_label(label), crate::ui_kit::Button::Normal);
|
||||
crate::panel_runtime::register_content_focus(ui.ctx(), &response);
|
||||
if response.clicked() {
|
||||
action = Some(Action::Export(ext));
|
||||
}
|
||||
}
|
||||
action
|
||||
};
|
||||
if shared { ui.horizontal_wrapped(buttons).inner } else { ui.horizontal(buttons).inner }
|
||||
}
|
||||
|
||||
fn options(ui: &mut egui::Ui, d: &mut BomPanel, shared: bool) {
|
||||
let was = (d.top, d.by_definition, d.group_children);
|
||||
let available = ui.available_width();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (option, labels) in [(&mut d.top, ["Parts only", "Top level"]), (&mut d.by_definition, ["By part number", "By definition"])] {
|
||||
let width = labels.iter().map(|label| crate::ui_kit::ContentWidth::button(ui, label)).fold(0.0, f32::max) * 2.0;
|
||||
let width = if shared { width.min(available) } else { width };
|
||||
let mut selected = usize::from(*option);
|
||||
ui.allocate_ui(egui::vec2(width, crate::ui_kit::row_height()), |ui| {
|
||||
crate::ui_kit::segmented(ui, &mut selected, &labels, Default::default());
|
||||
});
|
||||
*option = selected == 1;
|
||||
ui.separator();
|
||||
}
|
||||
if !shared {
|
||||
crate::ui_kit::checkbox(ui, solidcraft_ui_text::tr("bom.group_children"), &mut d.group_children);
|
||||
}
|
||||
});
|
||||
if shared {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Wrap);
|
||||
let response = crate::ui_kit::checkbox(ui, solidcraft_ui_text::tr("bom.group_children"), &mut d.group_children);
|
||||
crate::panel_runtime::register_content_focus(ui.ctx(), &response);
|
||||
});
|
||||
if let Some(row) = apply_row {
|
||||
apply(app, &mut d, &row);
|
||||
}
|
||||
if let Some(ext) = export {
|
||||
let mut params = d.params();
|
||||
params["format"] = json!(ext);
|
||||
app.pick_save_command(&format!("bill-of-materials.{ext}"), &[ext], "assembly.bom", params);
|
||||
if was != (d.top, d.by_definition, d.group_children) {
|
||||
d.clear_pick();
|
||||
}
|
||||
if !closed {
|
||||
app.bom = Some(d);
|
||||
}
|
||||
|
||||
fn report(app: &mut SolveApp, ui: &mut egui::Ui, d: &mut BomPanel, shared: bool) -> (Option<Value>, f32) {
|
||||
options(ui, d, shared);
|
||||
let bom = d.rows(app);
|
||||
let rows = bom["rows"].as_array().map_or(&[][..], Vec::as_slice);
|
||||
d.reconcile_pick(rows);
|
||||
ui.separator();
|
||||
let widths = column_widths(ui, rows, shared);
|
||||
let mut grid = |ui: &mut egui::Ui| table(ui, d, rows, widths);
|
||||
let t = Tokens::get();
|
||||
let table_extra = if shared {
|
||||
let summary = crate::ui_kit::row_height() + ui.spacing().item_spacing.y;
|
||||
let scroll = egui::ScrollArea::both().id_salt("table").min_scrolled_height(0.0).max_height((ui.available_height() - summary).max(0.0)).show(
|
||||
ui,
|
||||
|ui| {
|
||||
grid(ui);
|
||||
ui.add_space(t.space_8);
|
||||
},
|
||||
);
|
||||
(scroll.content_size.y.min(REPORT_MAX_HEIGHT) - scroll.inner_rect.height()).max(0.0)
|
||||
} else {
|
||||
egui::ScrollArea::vertical().id_salt("table").max_height(REPORT_MAX_HEIGHT).show(ui, grid);
|
||||
0.0
|
||||
};
|
||||
ui.label(RichText::new(format!("{} rows, {} placed", rows.len(), text(&bom, "total_occurrences"))).small().color(t.text_dim));
|
||||
(d.picked.and_then(|i| rows.get(i)).cloned(), table_extra)
|
||||
}
|
||||
|
||||
fn table(ui: &mut egui::Ui, d: &mut BomPanel, rows: &[Value], widths: [f32; 6]) {
|
||||
let t = Tokens::get();
|
||||
// The item cell (a picker for a row, dim text for a sub-part) is drawn by the caller.
|
||||
let rest = |ui: &mut egui::Ui, row: &Value| {
|
||||
for (i, key) in COLUMN_KEYS.iter().enumerate().skip(1) {
|
||||
let dim = *key == "definition";
|
||||
cell(ui, widths[i], |ui| ui.label(if dim { RichText::new(text(row, key)).color(t.text_dim) } else { RichText::new(text(row, key)) }));
|
||||
}
|
||||
ui.end_row();
|
||||
};
|
||||
// No minimum column width: padding a narrow column would push the grid wider.
|
||||
egui::Grid::new("sc_bom_grid").num_columns(6).striped(true).min_col_width(0.0).spacing([COLUMN_SPACING, 4.0]).show(ui, |ui| {
|
||||
for (h, w) in HEADERS.iter().zip(widths) {
|
||||
cell(ui, w, |ui| ui.label(RichText::new(*h).strong().color(t.text_dim)));
|
||||
}
|
||||
ui.end_row();
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let picked = d.picked == Some(i);
|
||||
let response = cell(ui, widths[0], |ui| crate::ui_kit::list_picker(ui, &i.to_string(), &text(row, "item"), picked));
|
||||
if picked || response.has_focus() {
|
||||
crate::panel_runtime::register_content_focus(ui.ctx(), &response);
|
||||
}
|
||||
if response.clicked() {
|
||||
if picked {
|
||||
d.clear_pick();
|
||||
} else {
|
||||
d.picked = Some(i);
|
||||
d.target = Some((row["components"].clone(), row["definition"].clone()));
|
||||
d.fields = BOM_KEYS.iter().map(|k| text(row, k)).collect();
|
||||
}
|
||||
}
|
||||
rest(ui, row);
|
||||
for c in row["children"].as_array().into_iter().flatten() {
|
||||
cell(ui, widths[0], |ui| ui.label(RichText::new(text(c, "item")).color(t.text_dim)));
|
||||
rest(ui, c);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn details(ui: &mut egui::Ui, d: &mut BomPanel, selected: bool, shared: bool) {
|
||||
if selected {
|
||||
let mut editor = |ui: &mut egui::Ui| {
|
||||
for (key, field) in BOM_KEYS.iter().zip(d.fields.iter_mut()) {
|
||||
let label = field_label(key);
|
||||
let name = ui.label(label);
|
||||
if shared {
|
||||
let response = crate::ui_kit::cell_editor(ui, ui.make_persistent_id(key), field, label, false).labelled_by(name.id);
|
||||
crate::panel_runtime::register_content_focus(ui.ctx(), &response);
|
||||
} else {
|
||||
ui.allocate_ui(egui::vec2(280.0, crate::ui_kit::row_height()), |ui| {
|
||||
crate::ui_kit::cell_editor(ui, ui.make_persistent_id(key), field, label, false).labelled_by(name.id);
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
};
|
||||
if shared {
|
||||
editor(ui);
|
||||
} else {
|
||||
egui::Grid::new("sc_bom_edit").num_columns(2).spacing([12.0, 6.0]).show(ui, editor);
|
||||
}
|
||||
}
|
||||
if let Some(e) = &d.error {
|
||||
ui.label(RichText::new(e).color(Tokens::get().error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +560,7 @@ mod tests {
|
||||
ui.set_max_width(width);
|
||||
let font = egui::TextStyle::Body.resolve(ui.style());
|
||||
let measure = |s: &str| ui.painter().layout_no_wrap(s.to_owned(), font.clone(), egui::Color32::PLACEHOLDER).size().x;
|
||||
out = (super::column_widths(ui, rows), texts.iter().map(|s| measure(s)).collect());
|
||||
out = (super::column_widths(ui, rows, false), texts.iter().map(|s| measure(s)).collect());
|
||||
});
|
||||
})
|
||||
.textures_delta
|
||||
|
||||
@@ -67,6 +67,8 @@ pub struct DocSlot {
|
||||
/// waits in the same slot, so showing the tab again picks up exactly where it was left.
|
||||
#[derive(Default)]
|
||||
struct DocWork {
|
||||
/// Shared panel content belongs to the design, independently of its current placement.
|
||||
bom: Option<crate::bom_panel::BomPanel>,
|
||||
dialog: Option<crate::dialogs::Dialog>,
|
||||
editing_state: Option<crate::dialogs::EditingState>,
|
||||
tool: Option<crate::tools::Tool>,
|
||||
@@ -79,12 +81,15 @@ struct DocWork {
|
||||
/// computing after OK can't wait (its result belongs to the window thread): that dialog is
|
||||
/// cancelled, as without the preference.
|
||||
fn take_work(app: &mut SolveApp) -> DocWork {
|
||||
let keep_bom = crate::panel_runtime::production_active(app);
|
||||
let bom = app.bom.take().filter(|_| keep_bom);
|
||||
if !app.preferences.keep_work_on_tab_switch || app.applying.is_some() {
|
||||
crate::dialogs::cancel(app);
|
||||
app.tool = None;
|
||||
return DocWork::default();
|
||||
return DocWork { bom, ..DocWork::default() };
|
||||
}
|
||||
DocWork {
|
||||
bom,
|
||||
dialog: app.dialog.take(),
|
||||
editing_state: app.editing_state.take(),
|
||||
tool: app.tool.take(),
|
||||
@@ -97,6 +102,10 @@ fn take_work(app: &mut SolveApp) -> DocWork {
|
||||
/// design's history rolled back for an edit, and only the dialog's Cancel or OK puts it back.
|
||||
/// `before`: the session's revision before the switch moved it on to refresh the views.
|
||||
fn put_work(app: &mut SolveApp, mut work: DocWork, before: u64) {
|
||||
if crate::panel_runtime::production_active(app) {
|
||||
app.bom = work.bom;
|
||||
crate::bom_panel::set_focus_pending(app, true);
|
||||
}
|
||||
// That refresh is not a change to the design: the edit's Cancel must still see the design
|
||||
// it left (`Session::restore_editor` compares revisions), as `PendingFeatureEdit` does.
|
||||
if let Some(state) = &mut work.editing_state {
|
||||
@@ -402,7 +411,6 @@ fn swap_in(app: &mut SolveApp, mut slot: DocSlot) -> DocSlot {
|
||||
crate::dim_view::cancel();
|
||||
app.params = None;
|
||||
app.design_properties = None;
|
||||
app.bom = None;
|
||||
app.cancel_view_animation();
|
||||
app.viewport.orbit_free = None;
|
||||
app.preview.reset();
|
||||
@@ -673,6 +681,7 @@ pub fn adopt(app: &mut SolveApp, session: Session) -> bool {
|
||||
crate::semantic_content::leave_document(app);
|
||||
crate::dialogs::cancel(app);
|
||||
app.tool = None;
|
||||
app.bom = None;
|
||||
crate::design_properties::close_object(app);
|
||||
crate::sketch_attachment_ui::clear(app);
|
||||
app.session.replace_design(session);
|
||||
@@ -827,6 +836,7 @@ pub fn close(app: &mut SolveApp, i: usize, force: bool) {
|
||||
let rev = app.session.revision;
|
||||
crate::dialogs::cancel(app);
|
||||
app.tool = None;
|
||||
app.bom = None;
|
||||
app.session.replace_design(Session::default());
|
||||
app.session.revision = rev + 1;
|
||||
crate::prefs::apply_new_design(app);
|
||||
|
||||
@@ -1691,6 +1691,7 @@ impl SolveApp {
|
||||
self.edit_in_place.active = None;
|
||||
render_workspace::reset_document(self);
|
||||
dim_view::cancel();
|
||||
self.bom = None;
|
||||
design_properties::close_object(self);
|
||||
timeline::stop_playback(self);
|
||||
self.menu.props = None;
|
||||
@@ -2201,7 +2202,11 @@ impl SolveApp {
|
||||
return;
|
||||
}
|
||||
if id == "assembly.bom" {
|
||||
bom_panel::open(self);
|
||||
if panel_runtime::production_active(self) && bom_panel::available(self) {
|
||||
bom_panel::set_focus_pending(self, true);
|
||||
} else {
|
||||
bom_panel::open(self);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Inspect → Measure → Properties: the window, not a reply nobody sees (#8095).
|
||||
@@ -3727,7 +3732,9 @@ impl SolveApp {
|
||||
params_dialog::show(self, ui.ctx());
|
||||
design_properties::show(self, ui.ctx());
|
||||
sketch_report::show(self, ui.ctx());
|
||||
bom_panel::show(self, ui.ctx());
|
||||
if !panel_runtime::production_active(self) {
|
||||
bom_panel::show(self, ui.ctx());
|
||||
}
|
||||
material_card_review::show(self, ui.ctx());
|
||||
recovery_ui::show(self, ui.ctx());
|
||||
mesh_units::show(self, ui.ctx());
|
||||
|
||||
@@ -27,9 +27,10 @@ enum Product {
|
||||
Assistant,
|
||||
RenderBrowser,
|
||||
SketchOptions,
|
||||
Bom,
|
||||
}
|
||||
impl Product {
|
||||
const ALL: [Self; 5] = [Self::Browser, Self::History, Self::Assistant, Self::RenderBrowser, Self::SketchOptions];
|
||||
const ALL: [Self; 6] = [Self::Browser, Self::History, Self::Assistant, Self::RenderBrowser, Self::SketchOptions, Self::Bom];
|
||||
|
||||
fn index(self) -> usize {
|
||||
match self {
|
||||
@@ -38,6 +39,7 @@ impl Product {
|
||||
Self::Assistant => 2,
|
||||
Self::RenderBrowser => 3,
|
||||
Self::SketchOptions => 4,
|
||||
Self::Bom => 5,
|
||||
}
|
||||
}
|
||||
fn visible(self, app: &crate::SolveApp) -> bool {
|
||||
@@ -46,6 +48,7 @@ impl Product {
|
||||
Self::History => app.ui.show_timeline,
|
||||
Self::Assistant => app.agent_chat.open,
|
||||
Self::SketchOptions => true,
|
||||
Self::Bom => crate::bom_panel::available(app),
|
||||
}
|
||||
}
|
||||
fn set_visible(self, app: &mut crate::SolveApp, open: bool) {
|
||||
@@ -54,6 +57,8 @@ impl Product {
|
||||
Self::History => app.ui.show_timeline = open,
|
||||
Self::Assistant => app.agent_chat.open = open,
|
||||
Self::SketchOptions => {}
|
||||
Self::Bom if !open => app.bom = None,
|
||||
Self::Bom => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +69,7 @@ impl Product {
|
||||
Self::Assistant => "assistant",
|
||||
Self::RenderBrowser => "render-browser",
|
||||
Self::SketchOptions => crate::sketch_palette::INSTANCE_ID,
|
||||
Self::Bom => "bom",
|
||||
}
|
||||
}
|
||||
fn kind(self) -> &'static str {
|
||||
@@ -73,6 +79,7 @@ impl Product {
|
||||
Self::Assistant => "solidcraft.assistant",
|
||||
Self::RenderBrowser => "solidcraft.render_browser",
|
||||
Self::SketchOptions => "solidcraft.sketch_options",
|
||||
Self::Bom => "solidcraft.bom",
|
||||
}
|
||||
}
|
||||
fn title_key(self) -> &'static str {
|
||||
@@ -82,6 +89,14 @@ impl Product {
|
||||
Self::Assistant => "ui.assistant",
|
||||
Self::RenderBrowser => "render.page_scene",
|
||||
Self::SketchOptions => "ui.palette_sketch_options",
|
||||
Self::Bom => crate::bom_panel::TITLE_KEY,
|
||||
}
|
||||
}
|
||||
fn icon(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bom => "bom",
|
||||
Self::SketchOptions => "sketch",
|
||||
_ => "list",
|
||||
}
|
||||
}
|
||||
fn from_instance(instance: &InstanceId) -> PanelResult<Self> {
|
||||
@@ -95,6 +110,7 @@ impl Product {
|
||||
Self::Assistant => crate::agent_chat::shown(app) && !app.preferences.agent_chat_popped,
|
||||
Self::RenderBrowser => render && !crate::render_ui::focused(app),
|
||||
Self::SketchOptions => !render && crate::sketch_palette::editing(app),
|
||||
Self::Bom => crate::bom_panel::available(app),
|
||||
}
|
||||
}
|
||||
fn header_actions(self) -> Vec<HeaderActionSpec> {
|
||||
@@ -112,7 +128,7 @@ impl Product {
|
||||
("by-look", "render.tree_by_look", "list"),
|
||||
("search", "render.tree_search", "search"),
|
||||
],
|
||||
Self::Browser | Self::SketchOptions => Vec::new(),
|
||||
Self::Browser | Self::SketchOptions | Self::Bom => Vec::new(),
|
||||
};
|
||||
entries.into_iter().map(|(id, key, icon)| HeaderActionSpec { id: ControlId(id.into()), label_key: key.into(), icon: icon.into() }).collect()
|
||||
}
|
||||
@@ -167,14 +183,14 @@ impl Product {
|
||||
pressed: Some(pressed),
|
||||
})
|
||||
.collect(),
|
||||
Self::Browser | Self::SketchOptions => Vec::new(),
|
||||
Self::Browser | Self::SketchOptions | Self::Bom => Vec::new(),
|
||||
};
|
||||
presentation
|
||||
}
|
||||
fn empty_presentation(self) -> PanelPresentation {
|
||||
PanelPresentation {
|
||||
title: solidcraft_ui_text::tr(self.title_key()).into(),
|
||||
icon: if self == Self::SketchOptions { "sketch" } else { "list" }.into(),
|
||||
icon: self.icon().into(),
|
||||
status: None,
|
||||
attention: false,
|
||||
actions: Vec::new(),
|
||||
@@ -205,6 +221,7 @@ struct ProductionSnapshot {
|
||||
panels: [PanelSnapshot; Product::ALL.len()],
|
||||
sketch_owner: Option<(DocumentId, u64)>,
|
||||
sketch_placement_attempt: Option<(u64, LogicalRect, u64)>,
|
||||
bom_focus_attempt: Option<(u64, LogicalRect)>,
|
||||
actions: VecDeque<QueuedAction>,
|
||||
}
|
||||
struct VisibilityIntent {
|
||||
@@ -227,13 +244,14 @@ impl ProductionState {
|
||||
document: DocumentId::new(app.session.document_id.clone())?,
|
||||
play_button: app.preferences.history_play_button,
|
||||
visible: Product::ALL.map(|product| product.visible(app)),
|
||||
visibility: [const { None }; Product::ALL.len()],
|
||||
visibility: Default::default(),
|
||||
panels: Product::ALL.map(|product| PanelSnapshot {
|
||||
product,
|
||||
available: product.available(app),
|
||||
presentation: product.presentation(app, app.preferences.history_play_button),
|
||||
extent: None,
|
||||
}),
|
||||
bom_focus_attempt: None,
|
||||
actions: VecDeque::new(),
|
||||
sketch_owner: None,
|
||||
sketch_placement_attempt: None,
|
||||
@@ -299,6 +317,7 @@ pub(crate) fn enable_production(app: &mut crate::SolveApp) -> PanelResult<()> {
|
||||
}
|
||||
let repaired = super::preferences::configure_preferences(&mut integration, &app.panel_layout_preferences, &mut catalogue)?;
|
||||
app.panel_runtime = Some(integration);
|
||||
crate::bom_panel::set_focus_pending(app, true);
|
||||
if repaired {
|
||||
super::preferences::report_notice(app);
|
||||
}
|
||||
@@ -311,6 +330,7 @@ pub(crate) fn refresh_production(app: &crate::SolveApp) -> PanelResult<()> {
|
||||
let document = DocumentId::new(app.session.document_id.clone())?;
|
||||
if snapshot.document != document {
|
||||
snapshot.sketch_placement_attempt = None;
|
||||
snapshot.bom_focus_attempt = None;
|
||||
for panel in &mut snapshot.panels {
|
||||
panel.extent = None;
|
||||
}
|
||||
@@ -377,7 +397,7 @@ pub(crate) fn drain_production_actions(app: &mut crate::SolveApp) -> PanelResult
|
||||
};
|
||||
app.run("view.render_browser", params).map_err(|reason| error(Rule::Availability, reason))?;
|
||||
}
|
||||
Product::Browser | Product::SketchOptions => {}
|
||||
Product::Browser | Product::SketchOptions | Product::Bom => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -388,7 +408,7 @@ pub(crate) fn production_active(app: &crate::SolveApp) -> bool {
|
||||
}
|
||||
|
||||
/// Existing view toggles update remembered placement only when their raw flag
|
||||
/// changes. Context changes and hidden bodies never reopen closed panels.
|
||||
/// changes. BOM opens from its document owner's explicit reveal request.
|
||||
pub(crate) fn sync_production_visibility(app: &mut crate::SolveApp) -> PanelResult<()> {
|
||||
let Some(integration) = app.panel_runtime.as_ref() else { return Ok(()) };
|
||||
let Some(state) = integration.production.as_ref() else { return Ok(()) };
|
||||
@@ -399,6 +419,9 @@ pub(crate) fn sync_production_visibility(app: &mut crate::SolveApp) -> PanelResu
|
||||
let layout = integration.runtime().layout().layout();
|
||||
let mut mutations = Vec::new();
|
||||
for product in Product::ALL {
|
||||
if product == Product::Bom {
|
||||
continue;
|
||||
}
|
||||
let visible = product.visible(app);
|
||||
if snapshot.visible.get(product.index()).copied() != Some(visible) {
|
||||
let instance = layout.instance(&InstanceId::new(product.id())?)?;
|
||||
@@ -417,7 +440,8 @@ pub(crate) fn sync_production_visibility(app: &mut crate::SolveApp) -> PanelResu
|
||||
refresh_production(app)?;
|
||||
app.dispatch_panel_layout(LayoutAction::Apply { expected_revision: Some(revision), mutations })?;
|
||||
}
|
||||
sync_sketch_options(app)
|
||||
sync_sketch_options(app)?;
|
||||
sync_bom(app)
|
||||
}
|
||||
|
||||
fn sync_sketch_options(app: &mut crate::SolveApp) -> PanelResult<()> {
|
||||
@@ -489,6 +513,55 @@ fn sync_sketch_options(app: &mut crate::SolveApp) -> PanelResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_bom(app: &mut crate::SolveApp) -> PanelResult<()> {
|
||||
let Some(integration) = app.panel_runtime.as_ref() else { return Ok(()) };
|
||||
let Some(state) = integration.production.clone() else { return Ok(()) };
|
||||
let mut snapshot = state.0.try_borrow_mut().map_err(|_| error(Rule::Lifecycle, "BOM focus is borrowed"))?;
|
||||
let layout = integration.runtime().layout().layout();
|
||||
let revision = integration.runtime().layout().inspect().revision;
|
||||
let mut mutations = Vec::new();
|
||||
let mut bom_focused = false;
|
||||
let mut bom_attempt = None;
|
||||
if crate::bom_panel::focus_pending(app) {
|
||||
let id = InstanceId::new(Product::Bom.id())?;
|
||||
if layout.containing(&id).is_ok_and(|section| !section.collapsed && section.active == id) {
|
||||
bom_focused = true;
|
||||
} else if let Some(attempt) = app.universal_panel_frame().map(|frame| (revision, frame.layout.capacity))
|
||||
&& Some(attempt) != snapshot.bom_focus_attempt
|
||||
{
|
||||
let mutation = if layout.instance(&id).is_ok() { Mutation::Focus { instance: id } } else { open_bom(integration, layout, id)? };
|
||||
mutations.push(mutation);
|
||||
bom_attempt = Some(attempt);
|
||||
bom_focused = true;
|
||||
}
|
||||
}
|
||||
if let Some(attempt) = bom_attempt {
|
||||
snapshot.bom_focus_attempt = Some(attempt);
|
||||
}
|
||||
drop(snapshot);
|
||||
if !mutations.is_empty() {
|
||||
app.dispatch_panel_layout(LayoutAction::Apply { expected_revision: Some(revision), mutations })?;
|
||||
}
|
||||
if bom_focused {
|
||||
crate::bom_panel::set_focus_pending(app, false);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_bom(integration: &AppPanelRuntime, layout: &Layout, instance: InstanceId) -> PanelResult<Mutation> {
|
||||
let kind = PanelKind::new(Product::Bom.kind())?;
|
||||
let spec = integration.runtime().layout().registry().get(&kind)?;
|
||||
let zone = spec.default_zone;
|
||||
let size = spec.default_size;
|
||||
let section = next_section_id_with_reservations(layout, &integration.preference_identity_reservations()?)?;
|
||||
let destination = if let Some(anchor) = layout.sections.iter().find(|section| section.location.zone() == zone) {
|
||||
Destination::Relative { section: anchor.id.clone(), placement: RelativePlacement::Below, new_section: section, new_column: None, size }
|
||||
} else {
|
||||
Destination::NewSection { section, index: layout.sections.len(), location: SectionLocation::Dock { zone }, size }
|
||||
};
|
||||
Ok(Mutation::Open { kind, instance, destination: Some(destination) })
|
||||
}
|
||||
|
||||
fn registry(app: &crate::SolveApp) -> PanelResult<PanelRegistry> {
|
||||
let mut registry = PanelRegistry::default();
|
||||
let tokens = solidcraft_theme::Tokens::get();
|
||||
@@ -499,6 +572,7 @@ fn registry(app: &crate::SolveApp) -> PanelResult<PanelRegistry> {
|
||||
let history = product == Product::History;
|
||||
let assistant = product == Product::Assistant;
|
||||
let sketch = product == Product::SketchOptions;
|
||||
let bom = product == Product::Bom;
|
||||
let mut allowed_zones = [Zone::Left, Zone::Right, Zone::Floating].into_iter().collect::<std::collections::BTreeSet<_>>();
|
||||
if history {
|
||||
allowed_zones.insert(Zone::Bottom);
|
||||
@@ -510,18 +584,20 @@ fn registry(app: &crate::SolveApp) -> PanelResult<PanelRegistry> {
|
||||
registry.register(PanelSpec {
|
||||
kind: PanelKind::new(product.kind())?,
|
||||
title_key: product.title_key().into(),
|
||||
icon: if sketch { "sketch" } else { "list" }.into(),
|
||||
icon: product.icon().into(),
|
||||
allowed_zones,
|
||||
default_zone: if history {
|
||||
Zone::Bottom
|
||||
} else if sketch {
|
||||
} else if sketch || bom {
|
||||
Zone::Right
|
||||
} else {
|
||||
Zone::Left
|
||||
},
|
||||
min_size: Size {
|
||||
width: 240.0,
|
||||
height: if assistant {
|
||||
height: if bom {
|
||||
200.0
|
||||
} else if assistant {
|
||||
120.0
|
||||
} else if sketch {
|
||||
150.0
|
||||
@@ -533,11 +609,11 @@ fn registry(app: &crate::SolveApp) -> PanelResult<PanelRegistry> {
|
||||
max_size: Size { width: MAX_LOGICAL, height: MAX_LOGICAL },
|
||||
edge_rule: if assistant { EdgeRule::PinOuter } else { EdgeRule::None },
|
||||
surface: PanelSurface::FollowZone,
|
||||
features: PanelFeatures { popout: false, hot_closable: assistant, closable: !sketch, ..Default::default() },
|
||||
features: PanelFeatures { popout: false, hot_closable: assistant || bom, closable: !sketch, ..Default::default() },
|
||||
side_height_policy: if assistant { SideHeightPolicy::Fill } else { SideHeightPolicy::Content },
|
||||
fixed_bottom_height: history.then_some(bottom_height),
|
||||
header_actions,
|
||||
text_heavy: assistant || sketch,
|
||||
text_heavy: assistant || sketch || bom,
|
||||
})?;
|
||||
}
|
||||
Ok(registry)
|
||||
@@ -550,7 +626,7 @@ fn default_layout(app: &crate::SolveApp, workspace: WorkspaceId, registry: &Pane
|
||||
let share = if app.preferences.browser_share.is_finite() { app.preferences.browser_share.clamp(0.05, 0.95) } else { 0.5 };
|
||||
for product in Product::ALL {
|
||||
let id = InstanceId::new(product.id())?;
|
||||
let requested_open = product.visible(app);
|
||||
let requested_open = product != Product::Bom && product.visible(app);
|
||||
let kind = PanelKind::new(product.kind())?;
|
||||
let spec = registry.get(&kind)?;
|
||||
let zone = match product {
|
||||
@@ -558,6 +634,7 @@ fn default_layout(app: &crate::SolveApp, workspace: WorkspaceId, registry: &Pane
|
||||
Product::Assistant if app.preferences.agent_chat_side == "right" => Zone::Right,
|
||||
Product::RenderBrowser if app.preferences.render_browser_side == "right" => Zone::Right,
|
||||
Product::SketchOptions => Zone::Right,
|
||||
Product::Bom => spec.default_zone,
|
||||
_ => Zone::Left,
|
||||
};
|
||||
let width = match product {
|
||||
@@ -566,7 +643,7 @@ fn default_layout(app: &crate::SolveApp, workspace: WorkspaceId, registry: &Pane
|
||||
Product::History => app.preferences.timeline_left_width,
|
||||
Product::Assistant => app.preferences.agent_chat_width,
|
||||
Product::RenderBrowser => app.preferences.render_browser_width,
|
||||
Product::SketchOptions => spec.default_size.width,
|
||||
Product::SketchOptions | Product::Bom => spec.default_size.width,
|
||||
};
|
||||
let width = if width.is_finite() { width.clamp(spec.min_size.width, 1200.0) } else { spec.default_size.width };
|
||||
let width = if product == Product::History && stacked {
|
||||
@@ -592,7 +669,7 @@ fn default_layout(app: &crate::SolveApp, workspace: WorkspaceId, registry: &Pane
|
||||
Product::Browser => app.preferences.browser_collapsed,
|
||||
Product::History => app.preferences.timeline_collapsed,
|
||||
Product::Assistant => app.preferences.agent_chat_minimized,
|
||||
Product::RenderBrowser | Product::SketchOptions => false,
|
||||
Product::RenderBrowser | Product::SketchOptions | Product::Bom => false,
|
||||
};
|
||||
let weight = match product {
|
||||
Product::Browser if stacked => share,
|
||||
@@ -944,7 +1021,7 @@ impl FramePanelContent for ProductionFrame<'_> {
|
||||
match product {
|
||||
Product::Browser | Product::RenderBrowser => self.app.shell.browser = Some(super::workspace::egui_rect(context.outer)),
|
||||
Product::History => self.app.shell.history = Some(super::workspace::egui_rect(context.outer)),
|
||||
Product::Assistant | Product::SketchOptions => {}
|
||||
Product::Assistant | Product::SketchOptions | Product::Bom => {}
|
||||
}
|
||||
let previous = self
|
||||
.state
|
||||
@@ -974,6 +1051,7 @@ impl FramePanelContent for ProductionFrame<'_> {
|
||||
Product::History => crate::timeline_left::panel_contents(self.app, ui),
|
||||
Product::Assistant => crate::agent_chat::shared_content(self.app, ui, context.flow),
|
||||
Product::SketchOptions => crate::sketch_palette::shared_body(self.app, ui),
|
||||
Product::Bom => crate::bom_panel::shared_content(self.app, ui),
|
||||
}
|
||||
let capture = ui.ctx().data_mut(|data| data.remove_temp::<Option<FocusCapture>>(focus_capture_id()).flatten());
|
||||
if context.document.as_ref().map(|document| document.0.as_str()) != Some(self.app.session.document_id.as_str()) {
|
||||
|
||||
+10
-1
@@ -63,10 +63,19 @@ the browser runs serially. Export memory, candidate lines, output lines and tota
|
||||
work remain bounded, and exceeding a budget returns an error without writing a partial drawing.
|
||||
|
||||
Programmatic callers use `file.export_view_dxf` with `camera: Camera`, `hidden: "omit"` or
|
||||
`"dashed"`, optional `tolerance` (0.0001–1 mm), optional `zero` and optional `path`. The drawing's
|
||||
`"dashed"`, optional `tolerance` (0.0001–1 mm), optional `zero`, optional `curves` and optional `path`. The drawing's
|
||||
0,0 is the point the camera looks at (`zero: "target"`, the default); with `zero: "origin"` it
|
||||
is the model's origin as seen from the camera, for machining from a part corner. Without `path`, the
|
||||
command returns the DXF text. The command does not modify the design or its undo history.
|
||||
|
||||
The view's curved edges are built from short straight lines, so by default a round hole is written
|
||||
as a ring of `LINE` entities. With `curves: "arcs"`, each ring of visible lines that lies on one
|
||||
circle is written as a single `CIRCLE`, and each run of lines that lies on one circle is written as
|
||||
an `ARC`, so a milling or CAD program can pick them as circles. A ring only counts as a circle when
|
||||
its lines sag from the circle by no more than the `tolerance` (twice, for rounding); a polygon with
|
||||
few sides, such as a hexagon, stays as lines. Points closer than a hundredth of the `tolerance` are
|
||||
treated as one point, so the top and bottom edges of a hole are written once. Dashed hidden lines
|
||||
and every other line stay as they are. The default is `curves: "lines"`.
|
||||
The result also includes `stats`: placed body and shared mesh counts, source triangle count,
|
||||
retained mesh bytes, worker limit, candidate segment count, occlusion work, and preparation,
|
||||
projection and occlusion times in milliseconds. These timings exclude STEP import and DXF
|
||||
|
||||
+151
-151
@@ -95,9 +95,9 @@
|
||||
{"case":"cylinder_ends_touch","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":59}}}
|
||||
{"case":"cylinder_native_kind","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14521.862698372206,14524.276911934576,53513.01760288019]],"n":82}}}
|
||||
{"case":"cylinder_vertical_disjoint","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"fuzz_axis_overflow_zero_y","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"fuzz_axis_overflow_zero_y_swapped","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"fuzz_axis_overflow_zero_y_trimmed","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"fuzz_axis_overflow_zero_y","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"fuzz_axis_overflow_zero_y_swapped","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"fuzz_axis_overflow_zero_y_trimmed","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"fuzz_general_04485","deferred":null,"error":"status_1001","ints":{},"topology":null,"values":{}}
|
||||
{"case":"fuzz_general_04639","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[1031.4996373619558,1031.4996373619558,153986.4298993043],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[807.9060988907511,807.9060988907511,125309.92224163261],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],["-inf","inf","-inf"],[583.155755420706,583.155755420706,96312.358800161],[942.2166899682857,942.2166899682857,86756.7926661245]],"n":7087}}}
|
||||
{"case":"fuzz_general_07170","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -126,26 +126,26 @@
|
||||
{"case":"gen0012","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0013","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0014","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14446.801070777872,14453.189016912429,50595.680338589]],"n":82}}}
|
||||
{"case":"gen0015","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0015","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0016","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14133.694223946348,14313.694224741723,44813.35360404759]],"n":72}}}
|
||||
{"case":"gen0017","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0018","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0019","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0020","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0019","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0020","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0021","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428906227,14432.828429476844,50068.95085778219]],"n":82}}}
|
||||
{"case":"gen0022","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[21896748.990274776,27229166.845889155,804605520.4184476]],"n":82}}}
|
||||
{"case":"gen0023","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0023","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0024","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0025","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0025","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0026","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0027","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0028","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0027","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0028","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0029","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0030","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0031","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0032","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14452.241706081204,14458.26550937229,50771.291903981044]],"n":82}}}
|
||||
{"case":"gen0033","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0034","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0034","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0035","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0036","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0037","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -154,7 +154,7 @@
|
||||
{"case":"gen0040","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0041","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0042","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0043","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0043","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0044","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85860.23871488366,85860.23871601537,9707695.670370897],[71612.98567436996,71792.98567587476,8506863.32425071],[56307.74696709108,56487.74696810789,4442451.538200121]],"n":760}}}
|
||||
{"case":"gen0045","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[18746814.463502917,27926395.22883291,647983017.0500393]],"n":82}}}
|
||||
{"case":"gen0046","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -179,18 +179,18 @@
|
||||
{"case":"gen0065","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0066","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0067","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0069","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14441.994748902653,14450.689580973994,50348.8376043799]],"n":82}}}
|
||||
{"case":"gen0070","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0071","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0072","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0073","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0073","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0074","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0075","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.82842862166,14432.828429938121,50068.95084011092]],"n":82}}}
|
||||
{"case":"gen0076","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0077","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0078","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0079","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0079","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0080","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428444305,14432.828429208652,50068.950838090255]],"n":82}}}
|
||||
{"case":"gen0081","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0082","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[42889.65686416142,42889.65686579953,2149219.05421427]],"n":196}}}
|
||||
@@ -242,7 +242,7 @@
|
||||
{"case":"gen0128","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0129","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0130","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0131","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0131","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0132","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0133","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0134","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -251,19 +251,19 @@
|
||||
{"case":"gen0137","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0138","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0139","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14716674.808262883,23663189.797756728,493181827.6223541]],"n":82}}}
|
||||
{"case":"gen0140","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0140","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0141","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0142","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0143","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0144","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0145","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0146","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0146","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0147","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[10439734.573744709,16103352.853689373,312233406.83566624]],"n":62}}}
|
||||
{"case":"gen0148","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[43383720.635029614,83523962.93332492,4290699057.6953125],[23549957.709665727,32385701.84115148,901688774.0002065]],"n":381}}}
|
||||
{"case":"gen0149","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0150","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0151","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0152","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0152","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0153","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14058.955851257828,14058.955851257828,43222.98967490412]],"n":42}}}
|
||||
{"case":"gen0154","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0155","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -297,28 +297,28 @@
|
||||
{"case":"gen0183","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0184","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0185","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0186","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0186","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0187","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85923.30150091987,85934.26544147397,9689703.845619688],[14108.348185235429,14110.93047373424,73579.6122594204]],"n":324}}}
|
||||
{"case":"gen0188","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0189","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[66197847.64265481,112539693.08704536,6971991534.637814],[35540787.249381885,44250672.16097885,1487809477.3242035]],"n":381}}}
|
||||
{"case":"gen0190","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0190","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0191","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0192","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0193","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0193","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0194","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0195","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828427954764,14432.828429190185,50068.95081487049]],"n":82}}}
|
||||
{"case":"gen0196","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828429596138,14432.828430611193,50068.950880705044]],"n":82}}}
|
||||
{"case":"gen0197","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0198","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14714099.135487935,36054925.86119919,1284848891.1701298]],"n":146}}}
|
||||
{"case":"gen0199","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[28719.969168760435,28888.905405553938,705511.7035161396]],"n":119}}}
|
||||
{"case":"gen0200","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0201","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0200","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0201","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0202","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[84470.6785529412,84854.18090904463,9496844.248370143],[14141.641512775144,14149.801572011182,355889.4422163861]],"n":344}}}
|
||||
{"case":"gen0203","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[213857504.40456918,239994361.48989153,26495297853.436]],"n":233}}}
|
||||
{"case":"gen0204","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0205","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0206","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0207","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0207","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0208","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[56360.980806056636,56370.81325019852,3811371.8996635466]],"n":193}}}
|
||||
{"case":"gen0209","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14444.065513407379,14447.746335496227,50473.77463281084]],"n":82}}}
|
||||
{"case":"gen0210","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -337,7 +337,7 @@
|
||||
{"case":"gen0223","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0224","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0225","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0226","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0226","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0227","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[12482256.205410399,14874794.614115372,381682692.71726835]],"n":62}}}
|
||||
{"case":"gen0228","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0229","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
@@ -356,14 +356,14 @@
|
||||
{"case":"gen0242","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0243","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0244","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0245","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0245","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0246","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[16727594.087707056,25741710.886498906,570853564.2701851]],"n":82}}}
|
||||
{"case":"gen0247","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13996.469201313803,14176.469201927224,42252.653694444576]],"n":62}}}
|
||||
{"case":"gen0248","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0249","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428033743,14432.828428826197,50068.950821861494]],"n":82}}}
|
||||
{"case":"gen0250","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0251","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14453.341523380552,14457.508084504998,50830.5226145696]],"n":82}}}
|
||||
{"case":"gen0252","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0252","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0253","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0254","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0255","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -373,7 +373,7 @@
|
||||
{"case":"gen0259","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828429059673,14432.828430214957,50068.9508585844]],"n":82}}}
|
||||
{"case":"gen0260","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0261","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14441.650638261752,14454.441934506176,50299.219581384205]],"n":82}}}
|
||||
{"case":"gen0262","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0262","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0263","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.82842871408,14432.828430113952,50068.950842970735]],"n":82}}}
|
||||
{"case":"gen0264","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0265","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[9194561.401463512,16063346.305070397,477484591.8671847]],"n":119}}}
|
||||
@@ -384,8 +384,8 @@
|
||||
{"case":"gen0270","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[56376.797644091865,56387.44311205826,3810891.3452157816]],"n":193}}}
|
||||
{"case":"gen0271","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[20105553.503143128,23655293.19786365,730496861.9047214]],"n":72}}}
|
||||
{"case":"gen0272","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828429429179,14432.828430572763,50068.95087306986]],"n":82}}}
|
||||
{"case":"gen0273","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0274","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0273","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0274","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0275","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[20026159.39810277,32009702.676038917,672982970.5111591]],"n":82}}}
|
||||
{"case":"gen0276","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0277","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[19971274.086121574,31569911.744581215,674250376.3322549]],"n":82}}}
|
||||
@@ -395,7 +395,7 @@
|
||||
{"case":"gen0281","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0282","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[118465937.29552768,138051097.05808675,15021695563.516933]],"n":196}}}
|
||||
{"case":"gen0283","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14441.520760292698,14447.427854500007,50355.04046966381]],"n":82}}}
|
||||
{"case":"gen0284","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0284","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0285","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0286","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14174.751152966845,14354.75115412864,45783.537309970045]],"n":82}}}
|
||||
{"case":"gen0287","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -420,17 +420,17 @@
|
||||
{"case":"gen0306","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14722.331477580214,14724.331480396635,53976.26424896887]],"n":52}}}
|
||||
{"case":"gen0307","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85409.21393336343,85460.44234101755,9470207.92489995],[28531.758872734525,28539.938943062687,771722.0134832902]],"n":381}}}
|
||||
{"case":"gen0308","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[12768498.24005863,14665739.52584928,381247394.782874]],"n":62}}}
|
||||
{"case":"gen0309","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0309","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0310","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828429177664,14432.828429617066,50068.95086950738]],"n":82}}}
|
||||
{"case":"gen0311","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0312","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0313","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0313","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0314","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13473571.541260216,17996241.484404862,400876440.0911029]],"n":62}}}
|
||||
{"case":"gen0315","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0316","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14049.599162400638,14233.312571183782,43378.246192721635]],"n":62}}}
|
||||
{"case":"gen0317","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0318","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0319","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0318","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0319","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0320","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[47390826.31162868,64467767.51824551,5195828062.392242]],"n":233}}}
|
||||
{"case":"gen0321","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0322","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428412433,14432.828429852207,50068.950830876674]],"n":82}}}
|
||||
@@ -442,7 +442,7 @@
|
||||
{"case":"gen0328","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0329","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0330","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0331","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0331","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0332","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428198913,14432.828429378367,50068.950824868116]],"n":82}}}
|
||||
{"case":"gen0333","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0334","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -459,23 +459,23 @@
|
||||
{"case":"gen0345","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14453.081887267374,14457.304087264452,50819.92498262769]],"n":82}}}
|
||||
{"case":"gen0346","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[23038006.311890375,36291419.62673658,778981586.9667969]],"n":82}}}
|
||||
{"case":"gen0347","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0348","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0348","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0349","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0350","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14452.272991150676,14458.847986358756,50767.63547690333]],"n":82}}}
|
||||
{"case":"gen0351","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0352","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0353","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0354","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0354","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0355","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0356","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0356","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0357","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14448.30957551488,14460.375276861441,50564.81682530708]],"n":82}}}
|
||||
{"case":"gen0358","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0359","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[52189882.800161764,105809383.11333677,4726199115.822186],[39778673.58950604,46460839.48022019,1686437527.501957]],"n":381}}}
|
||||
{"case":"gen0360","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14441.32188208288,14449.211824825255,50329.765628172274]],"n":82}}}
|
||||
{"case":"gen0361","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0362","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14441.317861524161,14446.479533387383,50353.73485555264]],"n":82}}}
|
||||
{"case":"gen0363","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0364","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0363","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0364","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0365","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428004228,14432.828429249887,50068.950816704855]],"n":82}}}
|
||||
{"case":"gen0366","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[23158650.41282636,31259098.882548638,829244273.645615]],"n":82}}}
|
||||
{"case":"gen0367","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[21340427.509912614,27341969.301538344,728566307.5843793]],"n":72}}}
|
||||
@@ -501,7 +501,7 @@
|
||||
{"case":"gen0387","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[57012.32964443109,57043.18140134008,4721002.161603102]],"n":233}}}
|
||||
{"case":"gen0388","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0389","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0390","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0390","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0391","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0392","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0393","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -524,12 +524,12 @@
|
||||
{"case":"gen0410","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0411","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[43471883.46668356,48895392.40801717,3862291683.6652207]],"n":193}}}
|
||||
{"case":"gen0412","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0413","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0413","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0414","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0415","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0415","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0416","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0417","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0418","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0418","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0419","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[28698.826502228432,28844.22031107846,703741.8366948918]],"n":119}}}
|
||||
{"case":"gen0420","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[7812927.086638372,18441611.091272477,209597155.43201974]],"n":82}}}
|
||||
{"case":"gen0421","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[12940581.611247968,22594253.024581887,417798975.2566028]],"n":82}}}
|
||||
@@ -572,14 +572,14 @@
|
||||
{"case":"gen0458","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[43025.891634572006,43047.153481204776,2166309.8627637005]],"n":196}}}
|
||||
{"case":"gen0459","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0460","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85401.72281682461,85450.53289193627,9472714.900624933],[28524.600213399277,28534.304983124453,772415.4709744545]],"n":381}}}
|
||||
{"case":"gen0461","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0461","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0462","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0463","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0464","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[11394524.009914452,19888727.56270336,367875925.6198738]],"n":82}}}
|
||||
{"case":"gen0465","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13999.943646726555,14179.943647340606,42311.433349692845]],"n":62}}}
|
||||
{"case":"gen0466","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0467","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0468","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0468","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0469","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828429620999,14432.82843092496,50068.95087911796]],"n":82}}}
|
||||
{"case":"gen0470","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0471","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14451.868463659635,14457.136421827758,50763.44829650196]],"n":82}}}
|
||||
@@ -599,7 +599,7 @@
|
||||
{"case":"gen0485","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0486","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0487","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[18206403.297458556,26795730.245434992,610164103.6998626]],"n":72}}}
|
||||
{"case":"gen0488","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0488","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0489","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0490","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0491","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14096.957937654843,14276.957937931049,44193.41514626298]],"n":72}}}
|
||||
@@ -616,7 +616,7 @@
|
||||
{"case":"gen0502","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0503","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[17101776.066404734,21666961.040757604,512192672.83649874]],"n":62}}}
|
||||
{"case":"gen0504","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[21507481.43282599,33639945.52909817,729322491.0912203]],"n":82}}}
|
||||
{"case":"gen0505","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0505","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0506","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0507","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0508","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -638,7 +638,7 @@
|
||||
{"case":"gen0524","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[20271468.079312082,27682317.799811207,708291823.6572701]],"n":72}}}
|
||||
{"case":"gen0525","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14444.296515853579,14455.367929695936,50417.41180485282]],"n":82}}}
|
||||
{"case":"gen0526","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0527","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0527","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0528","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14059908.338799085,18431816.230761584,420075976.7763846]],"n":62}}}
|
||||
{"case":"gen0529","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[10750005.150267366,17095150.463120107,361793613.2859838]],"n":82}}}
|
||||
{"case":"gen0530","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[26587740.996793084,44944264.821638696,2922671514.5798497]],"n":233}}}
|
||||
@@ -654,10 +654,10 @@
|
||||
{"case":"gen0540","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14440.952908640153,14448.744386004422,50316.27506215421]],"n":82}}}
|
||||
{"case":"gen0541","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[43116.069775662916,43153.31907922553,2177905.9747778503]],"n":196}}}
|
||||
{"case":"gen0542","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0543","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0543","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0544","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.82842845218,14432.82842896397,50068.95084063005]],"n":82}}}
|
||||
{"case":"gen0545","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0546","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0546","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0547","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14433.228428983784,14435.228430012223,50074.35086435037]],"n":82}}}
|
||||
{"case":"gen0548","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[20573193.074012443,25121892.51115694,760020040.3841215]],"n":82}}}
|
||||
{"case":"gen0549","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -696,7 +696,7 @@
|
||||
{"case":"gen0582","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0583","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[17276411.46476069,23578023.005600873,526063291.8654253]],"n":62}}}
|
||||
{"case":"gen0584","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0585","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0585","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0586","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0587","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[32743555.123839077,36474227.87994416,2958374204.8852525]],"n":193}}}
|
||||
{"case":"gen0588","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -704,7 +704,7 @@
|
||||
{"case":"gen0590","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0591","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0592","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0593","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0593","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0594","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0595","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428999544,14432.828430406606,50068.950854017545]],"n":82}}}
|
||||
{"case":"gen0596","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -712,7 +712,7 @@
|
||||
{"case":"gen0598","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0599","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14106.925355602896,14286.925356063311,44361.6816418028]],"n":72}}}
|
||||
{"case":"gen0600","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0601","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0601","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0602","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[17040977.065711044,21431451.97563161,507667317.7559283]],"n":62}}}
|
||||
{"case":"gen0603","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13280613.736800453,22237425.931502204,437196023.53317684]],"n":82}}}
|
||||
{"case":"gen0604","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -749,8 +749,8 @@
|
||||
{"case":"gen0635","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0636","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0637","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0638","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0639","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0638","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0639","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0640","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[20991844.34525325,36354279.77880715,1012903614.9294618]],"n":109}}}
|
||||
{"case":"gen0641","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0642","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -762,7 +762,7 @@
|
||||
{"case":"gen0648","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0649","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0650","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14142.258699739601,14326.00221250205,45055.72019497898]],"n":72}}}
|
||||
{"case":"gen0651","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0651","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0652","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[19786507.982195623,27354906.96792105,652677713.6093085]],"n":72}}}
|
||||
{"case":"gen0653","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0654","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
@@ -830,7 +830,7 @@
|
||||
{"case":"gen0716","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0717","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14455.838645308062,14460.392642629284,50924.29020815579]],"n":82}}}
|
||||
{"case":"gen0718","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0719","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0719","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0720","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14082477.082031174,25918843.82415226,442942863.02341104]],"n":82}}}
|
||||
{"case":"gen0721","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0722","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85893.10566476198,85893.1056665154,9996613.451993113],[70794.93560604443,71154.93560939301,9077159.380443221],[28283.31047536967,28283.310475800114,1151840.8981686516]],"n":666}}}
|
||||
@@ -839,7 +839,7 @@
|
||||
{"case":"gen0725","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14441.498883739272,14453.360245399313,50301.53623294747]],"n":82}}}
|
||||
{"case":"gen0726","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14442.62091905672,14454.6957330435,50343.32085818934]],"n":82}}}
|
||||
{"case":"gen0727","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0728","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0728","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0729","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13989.36013133304,14174.367119653889,42230.16958609779]],"n":62}}}
|
||||
{"case":"gen0730","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0731","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85407.47847084492,85457.90625837643,9473909.230028225],[28526.032461913186,28536.51962052474,772615.706599606]],"n":381}}}
|
||||
@@ -862,7 +862,7 @@
|
||||
{"case":"gen0748","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0749","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0750","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0751","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0751","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0752","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14444.891599318373,14455.722853369949,50442.6974995792]],"n":82}}}
|
||||
{"case":"gen0753","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0754","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14445.732731824697,14457.02368733671,50471.371205061616]],"n":82}}}
|
||||
@@ -875,7 +875,7 @@
|
||||
{"case":"gen0761","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0762","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[7226389.887597533,10851080.821064087,248703128.1650216]],"n":82}}}
|
||||
{"case":"gen0763","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0764","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0764","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0765","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0766","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[22906785.964482203,29846414.97241395,829706102.8798732]],"n":82}}}
|
||||
{"case":"gen0767","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14444.066475716942,14455.48671278195,50405.37352723121]],"n":82}}}
|
||||
@@ -903,12 +903,12 @@
|
||||
{"case":"gen0789","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0790","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0791","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[9961361.411462344,13449364.000126243,343296943.52251214]],"n":72}}}
|
||||
{"case":"gen0792","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0792","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0793","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0794","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0795","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0796","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14455.030402519418,14462.613771187482,50866.04326358842]],"n":82}}}
|
||||
{"case":"gen0797","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0797","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0798","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0799","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428176514,14432.82842950949,50068.9508226388]],"n":82}}}
|
||||
{"case":"gen0800","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14440.29204450731,14445.524508413044,50313.18173127527]],"n":82}}}
|
||||
@@ -923,11 +923,11 @@
|
||||
{"case":"gen0809","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0810","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0811","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0812","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0812","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0813","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[254892113.11309904,291917206.83058596,31566546651.81496]],"n":196}}}
|
||||
{"case":"gen0814","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428989618,14432.82842991234,50068.95085791433]],"n":82}}}
|
||||
{"case":"gen0815","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0816","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0816","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0817","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0818","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0819","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -935,7 +935,7 @@
|
||||
{"case":"gen0821","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[9745343.268244695,14810352.061023552,334009966.4920724]],"n":82}}}
|
||||
{"case":"gen0822","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0823","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[85396.78359475142,85439.20076373992,9471283.457227968],[28523.098034703828,28530.599721390157,772087.510657516]],"n":381}}}
|
||||
{"case":"gen0824","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0824","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0825","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[15161653.919964613,29575405.798042633,462155413.0469962]],"n":82}}}
|
||||
{"case":"gen0826","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0827","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -949,8 +949,8 @@
|
||||
{"case":"gen0835","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14014.77068338863,14197.263506582716,42740.27629337135]],"n":62}}}
|
||||
{"case":"gen0836","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0837","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0838","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0839","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0838","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0839","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0840","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14448.83526048516,14463.03097751455,50566.442244353806]],"n":82}}}
|
||||
{"case":"gen0841","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0842","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -962,7 +962,7 @@
|
||||
{"case":"gen0848","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[43521.496365715226,43551.05974892291,2048196.28791872]],"n":146}}}
|
||||
{"case":"gen0849","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0850","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.207115258654,14463.448272322621,50580.51385396078]],"n":82}}}
|
||||
{"case":"gen0851","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0851","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0852","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0853","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14448.1830351362,14453.04538017699,50623.58985658099]],"n":82}}}
|
||||
{"case":"gen0854","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14452.299445587145,14458.721993629231,50770.013212350525]],"n":82}}}
|
||||
@@ -991,7 +991,7 @@
|
||||
{"case":"gen0877","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0878","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0879","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0880","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0880","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0881","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0882","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0883","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[43777080.83281214,82366462.76360479,4339140709.747036],[24948353.999551505,32633506.563223876,1002501260.7217215]],"n":381}}}
|
||||
@@ -1043,15 +1043,15 @@
|
||||
{"case":"gen0929","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14120.261673378986,14300.261673608888,44586.74707725021]],"n":72}}}
|
||||
{"case":"gen0930","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14443.691091354902,14454.197623724032,50398.84248063744]],"n":82}}}
|
||||
{"case":"gen0931","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0932","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0932","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0933","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0934","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[114896047.15225494,132766911.17818329,14402618849.375202]],"n":196}}}
|
||||
{"case":"gen0935","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0936","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14138.684399426385,14322.439259610977,45056.78440897368]],"n":72}}}
|
||||
{"case":"gen0937","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0938","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0939","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0940","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0938","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0939","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0940","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0941","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0942","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[23711182.585434206,36536548.658773094,858584977.3769872]],"n":82}}}
|
||||
{"case":"gen0943","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
@@ -1062,11 +1062,11 @@
|
||||
{"case":"gen0948","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.494629113007,14463.054105880427,50597.73254717154]],"n":82}}}
|
||||
{"case":"gen0949","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[17819178.72779256,21002856.183860153,603077398.419806]],"n":72}}}
|
||||
{"case":"gen0950","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14182.970237771266,14362.970238297674,45922.125877109036]],"n":82}}}
|
||||
{"case":"gen0951","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0951","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0952","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0953","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[25338340.06536581,38452136.46601765,869750396.1184317]],"n":82}}}
|
||||
{"case":"gen0954","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0955","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0955","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0956","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0957","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428348914,14432.828429335104,50068.950832415445]],"n":82}}}
|
||||
{"case":"gen0958","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -1102,7 +1102,7 @@
|
||||
{"case":"gen0988","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0989","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen0990","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[16916485.579241265,27465868.041858647,564629509.7886899]],"n":82}}}
|
||||
{"case":"gen0991","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0991","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0992","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0993","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.82842886563,14432.828430029165,50068.95085095921]],"n":82}}}
|
||||
{"case":"gen0994","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
@@ -1110,9 +1110,9 @@
|
||||
{"case":"gen0996","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen0997","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0998","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0999","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen0999","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1000","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14138.233334877277,14318.23333515817,44889.90985742875]],"n":72}}}
|
||||
{"case":"gen1001","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1001","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1002","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1003","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1004","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -1120,7 +1120,7 @@
|
||||
{"case":"gen1006","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1007","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[26454373.180785414,40774231.497762576,902523894.7277138]],"n":82}}}
|
||||
{"case":"gen1008","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428847608,14432.828429772142,50068.95085237073]],"n":82}}}
|
||||
{"case":"gen1009","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1009","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1010","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1011","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1012","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -1140,7 +1140,7 @@
|
||||
{"case":"gen1026","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14452.180730429167,14459.224330625906,50759.90067262237]],"n":82}}}
|
||||
{"case":"gen1027","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[28718.64971971652,28854.63875948619,702875.8392128487]],"n":109}}}
|
||||
{"case":"gen1028","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1029","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1029","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1030","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1031","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14844525.909745233,18137605.40531689,512384757.7004918]],"n":72}}}
|
||||
{"case":"gen1032","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -1149,7 +1149,7 @@
|
||||
{"case":"gen1035","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1036","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1037","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1038","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1038","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1039","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1040","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14454.285642113815,14468.015120733393,50782.70631483405]],"n":82}}}
|
||||
{"case":"gen1041","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14455.257645573953,14473.798152689838,50908.90949306811]],"n":82}}}
|
||||
@@ -1159,14 +1159,14 @@
|
||||
{"case":"gen1045","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[28694.486974167346,28844.10369105867,704128.8856809825]],"n":119}}}
|
||||
{"case":"gen1046","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen1047","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1048","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1048","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1049","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1050","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14445.455786719105,14449.219476511333,50527.15442943693]],"n":82}}}
|
||||
{"case":"gen1051","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1051","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1052","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1053","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1054","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[24778239.591827903,36620807.243472636,859191315.4136915]],"n":82}}}
|
||||
{"case":"gen1055","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1055","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1056","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1057","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1058","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -1216,7 +1216,7 @@
|
||||
{"case":"gen1102","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1103","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen1104","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.82842800658,14432.828428806957,50068.95082073397]],"n":82}}}
|
||||
{"case":"gen1105","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1105","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen1106","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1107","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13810097.375014553,19734863.737397242,453761285.20059437]],"n":72}}}
|
||||
{"case":"gen1108","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428995497,14432.828429939858,50068.95085795167]],"n":82}}}
|
||||
@@ -1225,7 +1225,7 @@
|
||||
{"case":"gen1111","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1112","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1113","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1114","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1114","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1115","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[28698.41784615772,28844.21131649129,703770.8684727347]],"n":119}}}
|
||||
{"case":"gen1116","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428823113,14432.828429980362,50068.95084935958]],"n":82}}}
|
||||
{"case":"gen1117","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -1238,12 +1238,12 @@
|
||||
{"case":"gen1124","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428861983,14432.828429321722,50068.95085704052]],"n":82}}}
|
||||
{"case":"gen1125","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1126","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1127","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1127","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1128","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14448.747526400906,14461.644409270843,50574.512843467885]],"n":82}}}
|
||||
{"case":"gen1129","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1130","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1131","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1132","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1132","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1133","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen1134","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14120.943970490049,14304.554830669817,44787.59560952401]],"n":72}}}
|
||||
{"case":"gen1135","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.82842854983,14432.828429455032,50068.95084095187]],"n":82}}}
|
||||
@@ -1259,7 +1259,7 @@
|
||||
{"case":"gen1145","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1146","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[13988.90629950841,14171.986129015942,42179.50989761127]],"n":62}}}
|
||||
{"case":"gen1147","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1148","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1148","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1149","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1150","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1151","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -1281,7 +1281,7 @@
|
||||
{"case":"gen1167","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14084.96262936971,14264.962630043969,43990.85057410623]],"n":72}}}
|
||||
{"case":"gen1168","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1169","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1170","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1170","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1171","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1172","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[28717.50453806843,28882.529211937028,705198.2119878718]],"n":119}}}
|
||||
{"case":"gen1173","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -1291,8 +1291,8 @@
|
||||
{"case":"gen1177","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1178","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1179","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1180","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1181","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1180","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1181","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1182","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[56692.16900848049,56712.626049977946,4127016.326639988]],"n":213}}}
|
||||
{"case":"gen1183","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1184","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
@@ -1301,7 +1301,7 @@
|
||||
{"case":"gen1187","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1188","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1189","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1190","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1190","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1191","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[11945613.232229516,19023601.74412911,401087627.6133322]],"n":72}}}
|
||||
{"case":"gen1192","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14432.828428412064,14432.828429050718,50068.95083794651]],"n":82}}}
|
||||
{"case":"gen1193","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[30602162.325667407,60354210.00533615,3085474678.728321],[13294401.252727358,21664932.35491307,405205651.9601214]],"n":381}}}
|
||||
@@ -1309,7 +1309,7 @@
|
||||
{"case":"gen1195","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[24595748.57767509,50638771.582117446,2080141719.5289626]],"n":146}}}
|
||||
{"case":"gen1196","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[134513007.01382968,149689981.88605765,16011921199.621614]],"n":196}}}
|
||||
{"case":"gen1197","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"gen1198","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"gen1198","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"gen1199","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"global_circle_swap0_inside0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.071067811863,14458.727922061356,50615.75692603697]],"n":82}}}
|
||||
{"case":"global_circle_swap0_inside1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.071067811863,14458.727922061356,50615.75692603697]],"n":82}}}
|
||||
@@ -1329,49 +1329,49 @@
|
||||
{"case":"global_tangent_cylinders_swap1_inside1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":42}}}
|
||||
{"case":"hyperbolic_general_requested","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"inside_threshold","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags10","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags11","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags12","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags13","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags14","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags15","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags2","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags3","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags4","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags5","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags6","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags7","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags8","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags9","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags10","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags11","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags12","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags13","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags14","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags15","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags2","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags3","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags4","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags5","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags6","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags7","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags8","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class1_flags9","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_both_trims_class0_flags0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags10","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags11","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags12","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags13","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags14","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags15","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags2","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags3","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags4","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags5","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags6","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags7","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags8","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class0_flags9","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags10","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags11","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags12","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags13","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags14","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags15","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags2","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags3","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags4","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags5","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags6","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags7","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags8","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_both_trims_class1_flags9","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_full_domain_control","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_global_equal_radius_cones","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_opposite_axis_flags0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_opposite_axis_flags1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_opposite_axis_flags2","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_opposite_axis_flags3","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_overlapping_holes","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_overlapping_holes_reversed","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_scale_large","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_scale_small","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_touch_or_disjoint_4","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_global_equal_radius_cones","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_opposite_axis_flags0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_opposite_axis_flags1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_opposite_axis_flags2","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_opposite_axis_flags3","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_overlapping_holes","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_overlapping_holes_reversed","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_scale_large","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_scale_small","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_touch_or_disjoint_4","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":59}}}
|
||||
{"case":"issue711_touch_or_disjoint_5","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"issue711_upper_trim_crossing_0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"issue711_upper_trim_crossing_1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
@@ -1391,22 +1391,22 @@
|
||||
{"case":"parallel_cones_swap1_inside0","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"parallel_cones_swap1_inside1","deferred":null,"error":null,"ints":{"status_and_more":[1000,1]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"parallel_cyl_a_vmax_only","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"parallel_cyl_a_vmin_only","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"parallel_cyl_a_vtrim","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"parallel_cyl_b_vtrim","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"parallel_cyl_both_vtrim","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"parallel_cyl_a_vmin_only","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"parallel_cyl_a_vtrim","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"parallel_cyl_b_vtrim","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"parallel_cyl_both_vtrim","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"parallel_cyl_full","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[["-inf","inf","-inf"]],"n":79}}}
|
||||
{"case":"plane_outside_swap0_inside0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"plane_outside_swap0_inside1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"plane_outside_swap1_inside0","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"plane_outside_swap1_inside1","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":[0.0,0.0,0.0,0.0,0.0]}}
|
||||
{"case":"rejected_below_unitize_threshold_axis","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_0_cone","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_0_other","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_3_cone","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_3_other","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_cylinders_cone","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_cylinders_other","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_below_unitize_threshold_axis","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_0_cone","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_0_other","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_3_cone","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_3_other","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_cylinders_cone","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_tiny_axis_cylinders_other","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_ub154_fuzz00656","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_ub154_fuzz09540","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_ub154_fuzz09540_global","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
@@ -1414,7 +1414,7 @@
|
||||
{"case":"rejected_ub154_short_seam_other","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_ub154_short_seam_other_global","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_ub154_zero_height_other","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_zero_axis","deferred":null,"error":"status_1007","ints":{},"topology":null,"values":{}}
|
||||
{"case":"rejected_zero_axis","deferred":null,"error":"status_1006","ints":{},"topology":null,"values":{}}
|
||||
{"case":"review_circle_swap0_inside0_negative","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.071067811863,14458.727922061356,50615.75692603697]],"n":82}}}
|
||||
{"case":"review_circle_swap0_inside0_omitted","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.071067811863,14458.727922061356,50615.75692603697]],"n":82}}}
|
||||
{"case":"review_circle_swap0_inside0_system_default","deferred":null,"error":null,"ints":{"status_and_more":[1000,0]},"topology":null,"values":{"geometry":{"blocks":[[14449.071067811863,14458.727922061356,50615.75692603697]],"n":82}}}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{"case":"fc0029","deferred":null,"error":null,"ints":{"graph":{"blocks":[[5530,1048087],[2353,345210],[3761,443563],[8649,1231166],[11139,1119692],[2807,380758],[9671,1671146],[11579,901800],[9952,1471130],[17163,2342567],[25869,3493282],[20544,2673297]],"n":3058}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[1081.0,1081.0,34592.0]],"n":47},"geometry":{"blocks":[[2.5599999999999832e+22,2.5599999999999832e+22,3.2896e+24],[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23],[730.9442733577434,858.0120734429167,64858.61790680906]],"n":741},"record":{"blocks":[[155544.66853515652,155579.92476181663,19058058.264398053],[154883.12568554367,154941.51880574072,20324220.57230179],[142520.7082231047,142596.41784981615,18212653.135235615],[154806.4561564725,154972.45735892767,19093646.369808033],[155252.40131436303,155354.1721140812,20406811.642981462],[140642.9719392665,140862.60866179518,17894260.4774287],[154878.0705418264,155054.22088204307,19128506.313185643],[141068.82604785042,141433.55588031362,17527611.500434674],[154195.75,154556.75,18712557.75],[154243.50602216198,154538.41890063672,19941564.44806239],[140215.5,140487.5,17565873.5],[154703.36469041018,154878.00912893322,18777456.488563403],[155390.14231139628,155692.55151878114,20250848.848574623],[90721.45120858589,90868.34903995402,12471432.217107799],[90039.03768967514,90765.50307723251,11466839.444810867],[90449.58048159658,90968.38407563255,10645319.095995698],[15006.304828672948,15071.278910385701,75017.01759442406]],"n":4141},"surface_owners":{"blocks":[[20.0,20.0,210.0]],"n":20},"uv_curve_owners":{"blocks":[[4138.0,4332.0,257084.0]],"n":188}}}
|
||||
{"case":"fc0030","deferred":null,"error":null,"ints":{"graph":{"blocks":[[5530,1048087],[2353,345210],[3761,443563],[8649,1231166],[11139,1119692],[2807,380758],[9671,1671146],[11579,901800],[9952,1471130],[17163,2342567],[25869,3493282],[20544,2673297]],"n":3058}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[1081.0,1081.0,34592.0]],"n":47},"geometry":{"blocks":[[2.5599999999999832e+22,2.5599999999999832e+22,3.2896e+24],[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23],[730.9442733577434,858.0120734429167,64858.61790680906]],"n":741},"record":{"blocks":[[155544.66853515652,155579.92476181663,19058058.264398053],[154883.12568554367,154941.51880574072,20324220.57230179],[142520.7082231047,142596.41784981615,18212653.135235615],[154806.4561564725,154972.45735892767,19093646.369808033],[155252.40131436303,155354.1721140812,20406811.642981462],[140642.9719392665,140862.60866179518,17894260.4774287],[154878.0705418264,155054.22088204307,19128506.313185643],[141068.82604785042,141433.55588031362,17527611.500434674],[154195.75,154556.75,18712557.75],[154243.50602216198,154538.41890063672,19941564.44806239],[140215.5,140487.5,17565873.5],[154703.36469041018,154878.00912893322,18777456.488563403],[155390.14231139628,155692.55151878114,20250848.848574623],[90721.45120858589,90868.34903995402,12471432.217107799],[90039.03768967514,90765.50307723251,11466839.444810867],[90449.58048159658,90968.38407563255,10645319.095995698],[15006.304828672948,15071.278910385701,75017.01759442406]],"n":4141},"surface_owners":{"blocks":[[20.0,20.0,210.0]],"n":20},"uv_curve_owners":{"blocks":[[4138.0,4332.0,257084.0]],"n":188}}}
|
||||
{"case":"fc0031","deferred":null,"error":null,"ints":{"graph":{"blocks":[[175,12385]],"n":121}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[1.2e+21,1.2e+21,7.8e+21]],"n":31},"record":{"blocks":[[86448.35010058085,86489.35010058085,5479421.844715244]],"n":164},"surface_owners":[1.0],"uv_curve_owners":[0.0,1.0,2.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0000","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0000","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[666.3426207657328,9877518.473090675,9934452.135103568]],"n":424},"record":{"blocks":[[140974.2238529436,141052.06708209866,18166303.110872176],[127326.48126003167,127483.81247437863,17674464.920045],[128058.53158445854,128242.78759522102,17395060.81315641],[91081.30411119803,45482006.46488623,-5192257.522908466],[60762.71681469282,60811.28318530718,5140971.371759845]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0001","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43114.67955433795,43198.273712923794,1292528.3194918414]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0002","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.6799999999999925e+22,1.4196e+24],[-69.63094524647565,356.3690547535241,-2111.0734133096885]],"n":337},"record":{"blocks":[[140553.5,140965.5,18120762.0],[126937.62293491785,127069.62293491785,17636628.660356514],[103742.12293491785,104116.12293491785,13264092.515550645],[75988.12293491783,76084.12293491783,8443981.054912265]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
|
||||
{"case":"gen0003","deferred":null,"error":null,"ints":{"graph":{"blocks":[[692,106331],[25,79]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.4000000000000084e+21,3.000000000000001e+22]],"n":56},"record":{"blocks":[[87521.25275474228,43841294.327081785,215143713.92267185]],"n":210},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
@@ -74,12 +74,12 @@
|
||||
{"case":"gen0011","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1310,157645],[2328,233932],[3582,458604],[6017,676996]],"n":985}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999983e+22,7.260000000000002e+23]],"n":240},"record":{"blocks":[[140978.11223594163,141050.11223594163,18182104.290797252],[116073.72147934415,19869133.721479345,138812144.75758588],[60799.62862549696,60828.572897406964,5870369.975098656]],"n":710},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-60.0,60.0,-1830.0]],"n":60}}}
|
||||
{"case":"gen0012","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1485,181651],[2387,261025],[3175,417519],[6648,902856]],"n":1019}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23]],"n":240},"record":{"blocks":[[157217.22447274308,157231.22447274308,19278198.216036484],[156157.13303146334,156419.82363523965,20447243.250780012],[141455.48021756337,141593.08071388188,17996368.187218934],[155107.52131219086,155348.7302645498,19149360.30337226],[104573.08452012748,104661.55858306825,11530544.626243796],[30393.128551147056,30431.024802910142,904071.2969962137]],"n":1370},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[405.0,465.0,7625.0]],"n":60}}}
|
||||
{"case":"gen0013","deferred":null,"error":null,"ints":{"graph":{"blocks":[[104,5428]],"n":99}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":24},"record":{"blocks":[[58410.52515099852,58471.59621881038,2310587.9923582436]],"n":116},"surface_owners":[1.0],"uv_curve_owners":[0.0,1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0015","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0015","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156266.41673182778,156339.18329645565,19149568.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0016","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141017.179985721,141089.179985721,18195074.009364583],[-2365113.6349892905,7511426.3650107095,-610955675.9733758],[2484410.6400285587,12360906.640028557,116552881.88048549]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0017","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
{"case":"gen0018","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1485,181651],[2387,261025],[3175,417519],[6648,902856]],"n":1019}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23]],"n":240},"record":{"blocks":[[157217.22447274308,157231.22447274308,19278198.216036484],[156157.13303146334,156419.82363523965,20447243.250780012],[141455.48021756337,141593.08071388188,17996368.187218934],[155107.52131219086,155348.7302645498,19149360.30337226],[104573.08452012748,104661.55858306825,11530544.626243796],[30393.128551147056,30431.024802910142,904071.2969962137]],"n":1370},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[405.0,465.0,7625.0]],"n":60}}}
|
||||
{"case":"gen0020","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43095.61413593047,43182.391162038235,1291504.8075562809]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0021","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0021","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[679.2477305943074,9877527.828234844,9936130.320583055]],"n":424},"record":{"blocks":[[140963.82226276476,141045.3103206927,18166285.505433835],[127314.98823878425,127477.26331205857,17673788.0908405],[128034.57956527523,128233.61473746217,17393155.311088845],[91092.90486821948,45481951.13323095,-5188756.241267705],[60762.71681469282,60811.28318530718,5140971.371759845]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0022","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,5.551115123125783e-17,8.881784197001252e-16,-1.0229128535520042,0.00001,-1234567.0,-1234567.0,1234567.0,1234567.0,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[28982.89523240007,21960831.59885814,123206142.72427092]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
{"case":"gen0023","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43077.5681949364,43200.43710303231,1290536.0254608104]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0024","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,5.0,0.6729906661212666,0.00001,-10.0,-5.0,0.0,4.999999999999999,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[29019.807194736488,29195.036543914903,492938.81897199847]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
@@ -89,7 +89,7 @@
|
||||
{"case":"gen0028","deferred":null,"error":null,"ints":{"graph":{"blocks":[[682,105286],[29,96]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.4e+21,2.9999999999999996e+22]],"n":56},"record":{"blocks":[[142982.44946801648,143164.44946801648,15937837.083377652],[30107.0,30287.0,771201.0]],"n":342},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0029","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43116.93725845537,43200.53141704122,1292649.522554986]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0030","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1296,157069],[2328,235553],[3587,458155],[6012,680472]],"n":985}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23]],"n":240},"record":{"blocks":[[155889.5,155901.5,19084516.0],[154524.26679056452,154754.26679056452,20294414.16484167],[140727.0,140747.0,17917951.0],[154823.9825193369,154889.9825193369,19101055.99218987],[103941.5,103945.5,11452164.0],[30322.62862549696,30351.572897406964,900460.083544985]],"n":1370},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[405.0,465.0,7625.0]],"n":60}}}
|
||||
{"case":"gen0031","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0031","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[615.8846177461533,14815740.328712061,14867121.579591237]],"n":424},"record":{"blocks":[[140987.2736643996,141060.86746726738,18167363.483604513],[127271.2210827841,127432.17815770011,17660390.497585837],[128002.63741952149,128193.04443412332,17391234.07712418],[91053.96848240259,65856740.31616496,77823261.30486597],[60762.71681469282,60811.28318530718,5140971.371759845]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0032","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0033","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141253.1029586434,141336.67310083122,18233727.58146279],[910456.8947865181,29741318.114338845,368938141.54290855],[-791133.6058079528,13482279.819274934,-89893885.21058506]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0034","deferred":null,"error":null,"ints":{"graph":{"blocks":[[682,105286],[29,96]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.4e+21,2.9999999999999996e+22]],"n":56},"record":{"blocks":[[142982.44946801648,143164.44946801648,15937837.083377652],[30107.0,30287.0,771201.0]],"n":342},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
@@ -99,14 +99,14 @@
|
||||
{"case":"gen0038","deferred":null,"error":null,"ints":{"graph":{"blocks":[[104,5428]],"n":99}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":24},"record":{"blocks":[[58410.52515099852,58471.59621881038,2310587.9923582436]],"n":116},"surface_owners":[1.0],"uv_curve_owners":[0.0,1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0039","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141125.56968884318,141205.72410907954,18211054.808841206],[856315.8199191247,28873797.151187282,281138581.99902],[-736999.2588899866,14771293.71724904,24109598.28267207]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0040","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199480],[1748,274861],[1932,107271]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000017e+21,2.628e+23]],"n":148},"record":{"blocks":[[143904.70981994172,148850.83660596592,18749454.884977996],[60967.80078066501,91192355.46067868,416542727.02352035]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
|
||||
{"case":"gen0041","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0041","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[497.9241612642068,4938881.308940844,4974862.109125945]],"n":424},"record":{"blocks":[[140837.707242262,140909.707242262,18158995.012850292],[127379.96612520468,127451.96612576956,17689936.104902413],[128135.32248121651,128227.32248121651,17415200.373455055],[91087.38044853529,20675896.316721305,72070360.68664481],[60762.71681469282,60820.78556864008,5140969.303322095]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0042","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141028.51753995748,141100.51753995748,18209140.71170544],[104020.45910069923,20901260.41429212,82962163.56631431],[15281.0,15292.597876826972,503374.6497665959]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0043","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.6799999999999933e+22,1.4196e+24],[97.07644014672621,273.0764401467262,4532.062022572823]],"n":337},"record":{"blocks":[[140827.5568614641,140999.5568614641,18163612.41307346],[127029.93663265301,127151.93663265301,17644141.082648907],[103806.82188705914,19857052.821887065,77472491.1227448],[75988.12293491783,76084.12293491783,8443981.054912265]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
|
||||
{"case":"gen0044","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
{"case":"gen0045","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0046","deferred":null,"error":null,"ints":{"graph":{"blocks":[[104,5428]],"n":99}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":24},"record":{"blocks":[[58410.52515099852,58471.59621881038,2310587.9923582436]],"n":116},"surface_owners":[1.0],"uv_curve_owners":[0.0,1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0047","deferred":null,"error":null,"ints":{"graph":{"blocks":[[692,106331],[25,79]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.4000000000000084e+21,3.000000000000001e+22]],"n":56},"record":{"blocks":[[87521.9169102232,40095667.79834579,194151991.58350927]],"n":210},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0048","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0048","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[606.2575447759023,4939172.362337735,4987194.028411555]],"n":424},"record":{"blocks":[[140935.6381299591,141013.4813591142,18164983.31700817],[127300.62823450065,127457.95944884761,17672849.589725796],[127966.47478588187,128150.73079664435,17388950.893938813],[91124.98240081035,22660748.32632806,-39751764.701571964],[60762.71681469282,60811.28318530718,5140971.371759845]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0049","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43106.36612398419,43189.960282570035,1292082.0195465337]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0050","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1485,181651],[2387,261025],[3175,417519],[6648,902856]],"n":1019}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0,14.0],"geometry":{"blocks":[[1.1999999999999975e+22,1.1999999999999975e+22,7.260000000000001e+23]],"n":240},"record":{"blocks":[[157217.22447274308,157231.22447274308,19278198.216036484],[156157.13303146334,156419.82363523965,20447243.250780012],[141455.48021756337,141593.08071388188,17996368.187218934],[155107.52131219086,155348.7302645498,19149360.30337226],[104573.08452012748,104661.55858306825,11530544.626243796],[30393.128551147056,30431.024802910142,904071.2969962137]],"n":1370},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[405.0,465.0,7625.0]],"n":60}}}
|
||||
{"case":"gen0051","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000025e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141092.90971071896,141221.69068258165,18207690.57520062],[104028.99814035674,65874074.66927339,338695340.2160862],[15280.999999999993,15402.906760956694,502964.8308552575]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
@@ -117,7 +117,7 @@
|
||||
{"case":"gen0056","deferred":null,"error":null,"ints":{"graph":{"blocks":[[103,5364]],"n":97}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0],"geometry":{"blocks":[[8e+20,8e+20,3.6e+21]],"n":27},"record":{"blocks":[[43110.22168583844,43193.81584442428,1292289.0023408125]],"n":96},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0057","deferred":null,"error":null,"ints":{"graph":{"blocks":[[692,106331],[25,79]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.400000000000004e+21,3.0000000000000004e+22]],"n":56},"record":{"blocks":[[87495.35549293598,19840768.588122904,71346522.82882571]],"n":210},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0058","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141186.52911685206,141258.52911685206,18221543.461588066],[-1515580.127978588,27179463.490194604,-332182873.19711703],[1634882.5731820147,14575708.153637713,36214671.04161818]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0059","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0059","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[576.4799120437912,9877403.191385778,9922263.828823194]],"n":424},"record":{"blocks":[[140960.93079587494,141032.93079587494,18166850.62474821],[127255.44270984929,127414.00046731443,17659903.3699522],[127999.14773026791,128178.73507590464,17389989.730407752],[91113.69032347939,43510619.86313032,96805260.33414717],[60762.71681469282,60811.28318530718,5140971.371759845]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0060","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,5.0,0.7261627726849257,0.00001,-10.0,-5.0,0.0,4.999999999999999,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[29020.551604228378,29195.780953406793,492969.18024484627]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
{"case":"gen0061","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,5.0,0.0,0.00001,-10.0,-5.0,0.0,4.999999999999999,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[29010.385325410793,29185.614674589207,492554.5413016432]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
{"case":"gen0062","deferred":null,"error":null,"ints":{"graph":{"blocks":[[682,105286],[29,96]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.4e+21,2.9999999999999996e+22]],"n":56},"record":{"blocks":[[142982.44946801648,143164.44946801648,15937837.083377652],[30107.0,30287.0,771201.0]],"n":342},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
@@ -128,12 +128,12 @@
|
||||
{"case":"gen0067","deferred":null,"error":null,"ints":{"graph":{"blocks":[[692,106331],[25,79]],"n":261}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0],"geometry":{"blocks":[[2.4e+21,2.400000000000004e+21,3.0000000000000004e+22]],"n":56},"record":{"blocks":[[87510.89747446822,19840766.897475913,71347892.72503424]],"n":210},"surface_owners":[1.0,1.0,1.0],"uv_curve_owners":[-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0,-1.0]}}
|
||||
{"case":"gen0068","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1360,199542],[1745,274520],[1935,107845]],"n":623}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0],"geometry":{"blocks":[[7.2e+21,7.200000000000021e+21,2.628e+23]],"n":148},"record":{"blocks":[[142267.17126916934,149545.78124175625,18538871.935180865],[60965.31864733237,112625909.83332224,505787772.57405066]],"n":460},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-36.0,36.0,-666.0]],"n":36}}}
|
||||
{"case":"gen0069","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141089.7399922187,141161.7399922187,18205050.581791796],[-2365178.595266464,27264383.404733535,-487506475.8921594],[2484414.4434644342,12360910.443464434,116552946.53889537]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0070","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2468,318735],[2755,382199],[5710,650567],[3534,632497],[6013,592519],[9779,1412900],[12928,1499698]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[431.5198428883937,624.652584117112,27447.906125791415]],"n":424},"record":{"blocks":[[156224.41673182778,156297.18329645565,19149526.127055034],[154598.40251211118,154985.87177838173,20301423.514221795],[140664.7203279845,140836.2024273099,17905887.2094762],[154920.03783534584,154938.03783534584,19116535.34318591],[154743.16176198598,154820.03053193798,20312026.989448104],[140446.0,140764.0,17885794.0],[154426.5,154613.5,19052446.0],[129684.24687969516,129844.4946593029,14955378.32523922],[91187.66304535375,91247.66304535375,11441009.498965224],[30370.71681469282,30387.28318530718,1022758.4248081051]],"n":2398},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[1377.0,1485.0,48069.0]],"n":108}}}
|
||||
{"case":"gen0070","deferred":null,"error":null,"ints":{"graph":{"blocks":[[2508,322157],[2737,381059],[5730,644350],[3544,630392],[6005,593789],[9923,1440534],[12774,1466844]],"n":1777}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[351.0,351.0,6552.0]],"n":27},"geometry":{"blocks":[[2.1599999999999874e+22,2.1599999999999874e+22,2.3436e+24],[594.8222151510424,9877422.385229284,9925664.76554154]],"n":424},"record":{"blocks":[[140987.5806059466,141132.53606978574,18177367.434808776],[127284.77791966916,127433.34429028352,17679154.597682603],[-2607615.076132305,7802272.205709531,-674350117.5061443],[2826820.740667466,34866179.62810588,243332542.68494007],[60769.55505308291,60896.49480655866,5141139.8104582215]],"n":1210},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-108.0,108.0,-5886.0]],"n":108}}}
|
||||
{"case":"gen0071","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000008e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141015.63183242833,141140.705756406,18197056.686032668],[104017.577173002,19857179.943658657,137194437.6835873],[15280.999999999996,15346.710572543841,503200.5403258097]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0072","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.656000000000001e+23]],"n":194},"record":{"blocks":[[141126.55577577362,141208.92947829273,18210184.246911705],[1996454.7804006687,26971628.436782654,564052878.2477629],[-1877125.5188201377,13679903.616293807,-116562562.13196112]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0074","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000025e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141078.67945932975,141262.36447332427,18213079.47648731],[-2365134.5339968833,49170000.150317684,-534069203.35257745],[2484425.6882363446,12360978.591230411,116552981.21678422]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
{"case":"gen0075","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
{"case":"gen0076","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1172,159991],[1948,311927],[978,28202]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.4e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[156360.52780532837,158006.30563557148,19068776.25026989],[153903.1388885975,156472.02783429623,20181493.582973957],[104892.00006401539,108480.00015670061,12407745.286694974],[258.6666741371155,509.33334589004517,2804.4445185661316]],"n":784},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[360.0,392.0,9416.0]],"n":32}}}
|
||||
{"case":"gen0076","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1191,161874],[1949,313933],[974,28428]],"n":573}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0],"geometry":{"blocks":[[6.4e+21,6.400000000000004e+21,2.0799999999999997e+23]],"n":133},"record":{"blocks":[[-3573955.801055106,13714426.230891941,-884935273.6169126],[3765521.6562693026,6237320.46813458,20670087.030879214]],"n":432},"surface_owners":[1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-32.0,32.0,-528.0]],"n":32}}}
|
||||
{"case":"gen0077","deferred":null,"error":null,"ints":{"graph":{"blocks":[[50,1663]],"n":71}},"topology":null,"values":{"edge_curve_owners":[0.0],"geometry":[1e+20,1e+20,1e+20,1e+20,0.0,5.0,0.0,0.00001,-10.0,-5.0,0.0,4.999999999999999,0.00001,-30.61467458920718,0.0,0.00001],"record":{"blocks":[[29010.385325410793,29185.614674589207,492554.5413016432]],"n":70},"surface_owners":[1.0],"uv_curve_owners":[-1.0,-1.0]}}
|
||||
{"case":"gen0079","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1881,231784],[3468,553413],[2259,207996],[5091,517645],[9023,1312991],[4471,251788]],"n":1396}},"topology":null,"values":{"edge_curve_owners":{"blocks":[[210.0,210.0,3080.0]],"n":21},"geometry":{"blocks":[[1.6799999999999925e+22,1.679999999999995e+22,1.4196e+24],[247.87446819416934,292.6310905949567,11255.605714095394]],"n":337},"record":{"blocks":[[140956.03803434453,141040.53803434453,18185296.49393398],[127239.77528722705,127311.77528722705,17668690.287975267],[-3599843.39161713,48252017.31114167,-789183361.4693285],[3779671.2271521315,11187235.78436824,114616369.86204277]],"n":1001},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-84.0,84.0,-3570.0]],"n":84}}}
|
||||
{"case":"gen0080","deferred":null,"error":null,"ints":{"graph":{"blocks":[[1277,174490],[2444,346534],[4114,669278],[778,14800]],"n":804}},"topology":null,"values":{"edge_curve_owners":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0],"geometry":{"blocks":[[9.6e+21,9.600000000000017e+21,4.6559999999999995e+23]],"n":194},"record":{"blocks":[[141128.8346859163,141277.75836806116,18216127.880413357],[104026.62263511342,42713585.78034298,99922002.43730126],[15280.999999999989,15439.341829183926,502889.88234780787]],"n":585},"surface_owners":[1.0,1.0,1.0,1.0,1.0,1.0],"uv_curve_owners":{"blocks":[[-48.0,48.0,-1176.0]],"n":48}}}
|
||||
|
||||
@@ -938,16 +938,16 @@
|
||||
{"case":"project_t1_Defeature_inset_cube","deferred":null,"error":null,"ints":{"ints":{"blocks":[[47930,5122480],[296707,44554780],[309460,40911679],[345533,44236194],[362459,47203695],[292090,38557231],[292883,37524451],[331374,43793436],[217059,27159090],[336182,55359386],[577506,74237299],[562971,68179744],[19793,3010560],[201462,34899246],[325148,40831608],[337758,42622846],[322464,40119291],[329164,43031841],[275320,32297793],[247650,36334365],[593181,75363291],[306977,20802440]],"n":5631},"kinds":{"blocks":[[8,1764],[173,22137],[144,15812],[30,6822],[106,13390],[119,15309],[106,13883],[115,14722],[34,1467],[0,0],[34,6703],[90,12508],[135,17644],[133,17190],[130,15979],[0,0],[0,0],[52,8242],[41,8326],[172,22342],[44,1560],[118,15171],[103,14398],[73,11716],[80,9264],[51,9876],[68,6470],[129,17045],[137,17440],[111,11197],[0,0],[43,7957],[5,51]],"n":8215}},"topology":null,"values":{"dbls":{"blocks":[[233.00008000000003,233.00008000000003,27216.01016],[170.00002,170.00002,22199.00102],[179.0,179.0,22291.0],[173.00006000000002,173.00006000000002,25081.013000000003],[181.50015000000002,181.50015000000002,22786.0193],[191.00025000000008,191.00025000000008,25954.027879999998],[191.00004,191.00004,22869.00304],[120.00000000000003,120.00000000000003,13246.000000000007],[131.50008999999997,131.50008999999997,20435.016009999992],[170.00020999999998,170.00021000000004,20268.035120000008],[15.000069999999997,15.000069999999997,152.00096]],"n":2584}}}
|
||||
{"case":"project_t2_Defeature_inset_cube","deferred":null,"error":null,"ints":{"ints":{"blocks":[[47930,5122480],[296707,44554780],[309460,40911679],[345533,44236194],[362459,47203695],[292090,38557231],[292883,37524451],[331374,43793436],[217059,27159090],[336182,55359386],[577506,74237299],[562971,68179744],[36350,6428275],[1058,143504],[210385,26303238],[401197,53790178],[263986,35768489],[348233,44558527],[337155,42371224],[328300,43060551],[338724,43520888],[329590,41633635],[306671,40010565],[340418,44273484],[333909,42939780],[298393,36792527],[199501,26532758],[200472,25873491],[541662,76161936],[578370,74922268],[595731,76889392],[437703,42742518],[6680,594988]],"n":8361},"kinds":{"blocks":[[8,1764],[173,22137],[144,15812],[30,6822],[106,13390],[119,15309],[106,13883],[115,14722],[34,1467],[0,0],[34,6703],[90,12508],[135,17644],[133,17190],[130,15979],[0,0],[0,0],[52,8242],[5,53],[77,15148],[174,22401],[170,22255],[119,10875],[0,0],[88,13228],[116,14838],[100,14086],[106,13583],[49,5289],[101,14242],[87,11073],[101,12935],[34,3133],[34,3983],[102,13173],[85,8521],[70,8375],[72,10318],[135,17765],[133,17313],[132,16590],[138,17440],[99,9455],[0,0],[0,0],[0,0],[44,7326],[36,2458]],"n":12177}},"topology":null,"values":{"dbls":{"blocks":[[233.00008000000003,233.00008000000003,27216.01016],[170.00002,170.00002,22199.00102],[179.0,179.0,22291.0],[173.00006000000002,173.00006000000002,25081.013000000003],[181.50015000000002,181.50015000000002,22786.0193],[216.00025000000008,216.00025000000008,31065.527879999998],[211.00007000000002,211.00007000000002,27888.0091],[181.75005000000002,181.75005000000002,21376.5045],[170.5,170.5,20867.5],[174.75,174.75,23208.5],[125.00000000000006,125.00000000000006,16279.75000000001],[106.99999999999999,106.99999999999999,15387.499999999998],[234.12515000000005,234.12515000000005,29744.76815],[174.37513999999987,174.37513999999987,19796.767659999994],[154.0003300000002,154.00033000000025,17331.554640000002]],"n":3816}}}
|
||||
{"case":"project_tols_inset_cube","deferred":null,"error":null,"ints":{"ints":{"blocks":[[47930,5122480],[296707,44554780],[309460,40911679],[345533,44236194],[362459,47203695],[292090,38557231],[292883,37524451],[331374,43793436],[217059,27159090],[336182,55359386],[577506,74237299],[562971,68179744],[19859,3021692],[152411,26727701],[331323,38325666],[349699,44457139],[324572,43276919],[327200,41689894],[325694,42714196],[280233,31855449],[212094,26089242],[523232,74760958],[578128,74319704],[99113,2187041]],"n":6072},"kinds":{"blocks":[[8,1764],[173,22137],[144,15812],[30,6822],[106,13390],[119,15309],[106,13883],[115,14722],[34,1467],[0,0],[34,6703],[90,12508],[135,17644],[133,17190],[130,15979],[0,0],[0,0],[52,8242],[27,5385],[169,21725],[97,6967],[68,11706],[107,13319],[63,6278],[104,12970],[69,6112],[70,9817],[80,8550],[75,9431],[139,17615],[137,17835],[120,13644],[0,0],[12,2802],[44,3414]],"n":8862}},"topology":null,"values":{"dbls":{"blocks":[[233.00008000000003,233.00008000000003,27216.01016],[170.00002,170.00002,22199.00102],[179.0,179.0,22291.0],[173.00006000000002,173.00006000000002,25081.013000000003],[181.50015000000002,181.50015000000002,22786.0193],[208.50025000000008,208.50025000000008,28521.527879999998],[190.00005000000002,190.00005000000002,23511.504699999998],[192.25,192.25,25948.0],[131.62500000000003,131.62500000000003,16791.0],[229.37711000000004,229.37711000000004,25049.756609999993],[172.26013999999995,172.26014,19102.17432999999]],"n":2790}}}
|
||||
{"case":"raw_vote_midpoint_gap1.5e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,0],"ints":{"blocks":[[219266,30181024],[1,7]],"n":264},"kinds":{"blocks":[[71,10843],[15,473]],"n":350},"retained_original_faces":[]},"topology":null,"values":{"dbls":{"blocks":[[16.800006999999994,31.400007000000002,734.3503860000001]],"n":86}}}
|
||||
{"case":"raw_vote_midpoint_gap1.5e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219285,30185634],[247309,29867980]],"n":484},"kinds":{"blocks":[[71,10843],[71,10376],[62,4881]],"n":688},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[44.918193818181805,74.11819381818184,4761.473973272727]],"n":204}}}
|
||||
{"case":"raw_vote_midpoint_gap1.9e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,0],"ints":{"blocks":[[219266,30181024],[1,7]],"n":264},"kinds":{"blocks":[[71,10843],[15,473]],"n":350},"retained_original_faces":[]},"topology":null,"values":{"dbls":{"blocks":[[16.800006999999994,31.400007000000002,734.3503860000001]],"n":86}}}
|
||||
{"case":"raw_vote_midpoint_gap1.9e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219285,30185634],[247309,29867980]],"n":484},"kinds":{"blocks":[[71,10843],[71,10376],[62,4881]],"n":688},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[44.918193818181805,74.11819381818184,4761.473973272727]],"n":204}}}
|
||||
{"case":"raw_vote_midpoint_gap1.5e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219284,30185399],[219235,24917858]],"n":468},"kinds":{"blocks":[[71,10843],[51,5794],[48,2346]],"n":638},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[33.60001199999999,62.800012,2896.7011099999995]],"n":170}}}
|
||||
{"case":"raw_vote_midpoint_gap1.5e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219284,30185399],[219235,24917858]],"n":468},"kinds":{"blocks":[[71,10843],[51,5794],[48,2346]],"n":638},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[33.60001199999999,62.800012,2896.7011099999995]],"n":170}}}
|
||||
{"case":"raw_vote_midpoint_gap1.9e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219284,30185399],[219235,24917858]],"n":468},"kinds":{"blocks":[[71,10843],[51,5794],[48,2346]],"n":638},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[33.60001199999999,62.800012,2896.7011099999995]],"n":170}}}
|
||||
{"case":"raw_vote_midpoint_gap1.9e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219284,30185399],[219235,24917858]],"n":468},"kinds":{"blocks":[[71,10843],[51,5794],[48,2346]],"n":638},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[33.60001199999999,62.800012,2896.7011099999995]],"n":170}}}
|
||||
{"case":"raw_vote_midpoint_gap1e-08_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[219285,30185634],[219235,24917858]],"n":468},"kinds":{"blocks":[[71,10843],[51,5794],[48,2346]],"n":638},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[33.60001199999999,62.800012,2896.7011099999995]],"n":170}}}
|
||||
{"case":"raw_vote_midpoint_gap1e-08_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,0],"ints":{"blocks":[[219266,30181024],[1,7]],"n":264},"kinds":{"blocks":[[71,10843],[15,473]],"n":350},"retained_original_faces":[]},"topology":null,"values":{"dbls":{"blocks":[[16.800006999999994,31.400007000000002,734.3503860000001]],"n":86}}}
|
||||
{"case":"raw_vote_vertex_gap1.5e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269332,32310897],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1.5e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,0],"ints":{"blocks":[[233432,33934680],[142173,6513205]],"n":394},"kinds":{"blocks":[[40,3891],[94,9117],[0,0]],"n":528},"retained_original_faces":[]},"topology":null,"values":{"dbls":{"blocks":[[25.20001100000001,26.500011000000022,1551.4409519999997]],"n":134}}}
|
||||
{"case":"raw_vote_vertex_gap1.9e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269332,32310897],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1.9e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,0],"ints":{"blocks":[[233432,33934680],[142173,6513205]],"n":394},"kinds":{"blocks":[[40,3891],[94,9117],[0,0]],"n":528},"retained_original_faces":[]},"topology":null,"values":{"dbls":{"blocks":[[25.20001100000001,26.500011000000022,1551.4409519999997]],"n":134}}}
|
||||
{"case":"raw_vote_vertex_gap1.5e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269331,32310788],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1.5e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269331,32310788],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1.9e-12_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269331,32310788],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1.9e-12_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269331,32310788],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1e-08_trim0","deferred":null,"error":null,"ints":{"face_counts":[1,1],"ints":{"blocks":[[233432,33934680],[269332,32310897],[248432,27258137]],"n":728},"kinds":{"blocks":[[40,3891],[94,9117],[48,5477],[84,7344]],"n":994},"retained_original_faces":[0]},"topology":null,"values":{"dbls":{"blocks":[[49.580016999999984,52.08001699999994,6239.842179999998],[0.8200029999999999,0.920003,4.720018]],"n":266}}}
|
||||
{"case":"raw_vote_vertex_gap1e-08_trim1","deferred":null,"error":null,"ints":{"face_counts":[1,0],"ints":{"blocks":[[233432,33934680],[142173,6513205]],"n":394},"kinds":{"blocks":[[40,3891],[94,9117],[0,0]],"n":528},"retained_original_faces":[]},"topology":null,"values":{"dbls":{"blocks":[[25.20001100000001,26.500011000000022,1551.4409519999997]],"n":134}}}
|
||||
{"case":"read_Boolean_BoltMBox1","deferred":null,"error":null,"ints":{"ints":{"blocks":[[51641,4026687],[2207,272385],[32253,7720160],[210341,28400205],[210488,27575763],[385820,52113267],[422073,54464090],[277991,24498083],[243800,40487295],[363842,47347123],[349845,46573067],[348658,45312870],[351066,44660479],[349151,43342717],[366210,46252343],[353355,45656935],[352112,44399861],[368944,47299491],[355011,46544187],[353364,45232662],[324053,39380016],[301795,37487007],[319313,40582751],[304305,39592477],[307442,38890590],[324643,41869163],[311019,40973958],[310013,39646536],[204446,25862789],[203941,25931875],[202548,25702858],[205982,25474301],[288279,44069719],[577555,74251231],[595307,76237501],[580492,74044117],[597937,75978969],[599010,77877346],[584463,75705840],[602033,77696498],[288630,19363693],[19213,2506881],[8375,356982]],"n":10838},"kinds":{"blocks":[[1,4],[0,0],[13,3216],[170,21542],[173,22153],[170,21997],[170,21812],[169,21368],[174,22244],[77,4615],[0,0],[0,0],[34,7451],[111,14406],[110,13932],[115,14867],[106,13390],[119,15309],[106,13883],[115,14722],[110,14370],[111,14154],[113,14582],[108,13605],[117,15032],[106,13583],[117,14955],[108,14075],[113,14379],[112,14551],[109,13822],[116,15008],[106,13540],[118,15190],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[43,9172],[137,17595],[134,17417],[129,16176],[137,17595],[134,17417],[133,16952],[136,17263],[137,17869],[133,17428],[107,10992],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[40,7440],[56,7128],[60,6930]],"n":15851}},"topology":null,"values":{"dbls":{"blocks":[[3952.6769567352458,12757.27888369637,717038.9906747453],[2395.1713864543735,12131.883900006893,197336.41240936416],[-1350.1781032945735,12117.71472599499,60183.66949343005],[4976.449107900679,13020.097130358256,804325.5622943203],[1499.1858333347834,3688.1523462647265,55010.80919035692],[5752.026990470106,5752.026990470106,911717.3206673578],[17767.71719020585,17767.71719020585,2502269.940002056],[8600.264250805792,8600.264250805792,1386782.6006281632],[11605.612138749599,11605.612138749599,1240997.268777746],[9483.757521653897,9483.757521653897,1278546.7829604216],[1183.4526308115107,1183.4526308115107,185244.13576585884],[415.9247044410294,415.9247044410294,45804.759629890395],[87.492233551533,87.492233551533,10611.338551797768],[1731.8913561514337,4902.916181117506,329053.47229258355],[1724.9618093135252,9784.678718611945,44272.11846378022],[5203.2885928236565,14516.582783031386,507032.012387523],[-2098.4044826720333,11030.611979151076,-471228.6602196129],[1837.9529143710913,11760.738501844096,514765.0130299121],[4439.471970010148,12114.62315582713,672247.4057295277],[-69.50435154032552,6088.235107608219,-12978.387476637095]],"n":5013}}}
|
||||
|
||||
-1
@@ -1762,7 +1762,6 @@ fn enable_fix(k: &mut Kernel, name: &str) -> Result<(), String> {
|
||||
"cover_wraps_across_closed_seam" => &mut k.fixes.cover_wraps_across_closed_seam,
|
||||
"fillet_refuse_non_manifold_result" => &mut k.fixes.fillet_refuse_non_manifold_result,
|
||||
"fillet_torus_rail_on_axis" => &mut k.fixes.fillet_torus_rail_on_axis,
|
||||
"cone_parallel_line_midpoint" => &mut k.fixes.cone_parallel_line_midpoint,
|
||||
"fillet_trace_from_tangent_point_toward_next" => {
|
||||
&mut k.fixes.fillet_trace_from_tangent_point_toward_next
|
||||
}
|
||||
|
||||
@@ -945,8 +945,7 @@ fn dump_curve(o: &mut Vec<f64>, c: Option<&Curve>) {
|
||||
fn status(e: &SolidError) -> f64 {
|
||||
match e {
|
||||
SolidError::InvalidInput { .. } | SolidError::PortInvariant { .. } => 1007.0,
|
||||
SolidError::Upstream { code, .. } => f64::from(*code),
|
||||
_ => -1.0,
|
||||
e => crate::output::blend_status(e).map_or(-1.0, f64::from),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+155
-18
@@ -13,9 +13,11 @@ use solidcore::geom::certified::Verdict;
|
||||
use solidcore::mass::integrator::properties::{brep_volume, no_unsure_region, PropertyAttributes};
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
|
||||
use solidcore::primitive::api_primitives as primitives;
|
||||
use solidcore::query::api_queries::brep_is_closed_touching_solids;
|
||||
use solidcore::topology::brep::mirror::transform_matrix;
|
||||
use solidcore::topology::brep::p2::is_manifold_solid;
|
||||
use solidcore::topology::brep::p3::{brep_copy, brep_copy_mapped};
|
||||
use solidcore::topology::brep::{is_outward_solid, is_outward_touching_solids};
|
||||
use solidcore::topology::brep::{is_outward_solid, is_valid_and_outward};
|
||||
use solidcore::topology::face::p2 as face_p2;
|
||||
use solidcore::topology::FaceId;
|
||||
use solidcore::{Brep, EntityRef, Vec3};
|
||||
@@ -29,31 +31,129 @@ use crate::split::copies_of;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A kernel body, with its own lock: the kernel's operations take `&mut Brep`, even to measure.
|
||||
/// Beside the B-rep it may hold the proof that the B-rep is valid and faces out ([`Outward`]),
|
||||
/// made on that very B-rep and dropped by any mutable access to it ([`BodyGuard`]): a proof
|
||||
/// cannot outlive a change of the body it proves (#14830).
|
||||
#[derive(Debug)]
|
||||
pub struct Body(Mutex<Brep>);
|
||||
pub struct Body(Mutex<Held>);
|
||||
|
||||
/// What a [`Body`]'s lock holds: the B-rep, and its proof while nothing has changed it.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Held {
|
||||
brep: Brep,
|
||||
outward: Option<Outward>,
|
||||
}
|
||||
|
||||
/// The proof that a B-rep is valid and faces out ([`is_valid_and_outward`] held on it), with
|
||||
/// the kernel settings it was made under. Made only by [`prove`], on the B-rep it is then kept
|
||||
/// beside; it stands for that B-rep only while it is unchanged ([`BodyGuard`]) and the
|
||||
/// kernel's settings are the same ([`Outward::holds`]).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Outward {
|
||||
settings: Box<Settings>,
|
||||
}
|
||||
|
||||
/// The kernel settings a proof depends on: its tolerances and the upstream fixes (which change
|
||||
/// the validity check and the enclosure), and the context's model size estimate, size ratio and
|
||||
/// zone tolerance (which tolerance queries read). A session names its fixes per call
|
||||
/// ([`crate::session::set_fixes`], [`crate::session::set_thread_fixes`]), so they can differ
|
||||
/// between two operations; a proof made under other settings is not taken.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct Settings {
|
||||
tolerances: solidcore::kernel::Tolerances,
|
||||
fixes: solidcore::kernel::UpstreamFixes,
|
||||
/// The context's stored model size estimate, size ratio and zone tolerance, by their bits.
|
||||
context: [u64; 3],
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
fn of(k: &Kernel) -> Settings {
|
||||
Settings {
|
||||
tolerances: k.tolerances,
|
||||
fixes: k.fixes,
|
||||
context: [
|
||||
k.context.stored_model_size_estimate().to_bits(),
|
||||
k.context.stored_large_small_size_ratio().to_bits(),
|
||||
k.context.this_zone_tol_3d().to_bits(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Outward {
|
||||
/// Whether the proof stands under `k`'s settings: the ones it was made under.
|
||||
fn holds(&self, k: &Kernel) -> bool {
|
||||
*self.settings == Settings::of(k)
|
||||
}
|
||||
}
|
||||
|
||||
/// The proof that `b` is valid and faces out, if it is ([`is_valid_and_outward`], the part of
|
||||
/// the solid tests after their topology test): the only maker of an [`Outward`].
|
||||
///
|
||||
/// # Errors
|
||||
/// The kernel's, from the validity check and the volume's sign.
|
||||
fn prove(k: &mut Kernel, b: &mut Brep) -> solidcore::Result<Option<Outward>> {
|
||||
Ok(is_valid_and_outward(k, b)?.then(|| Outward {
|
||||
settings: Box::new(Settings::of(k)),
|
||||
}))
|
||||
}
|
||||
|
||||
/// A [`Body`] locked: the B-rep to read through `Deref`; through `DerefMut` it is handed out
|
||||
/// only once the proof beside it is dropped, so no change of the B-rep keeps a proof made
|
||||
/// before it.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BodyGuard<'a>(MutexGuard<'a, Held>);
|
||||
|
||||
impl std::ops::Deref for BodyGuard<'_> {
|
||||
type Target = Brep;
|
||||
fn deref(&self) -> &Brep {
|
||||
&self.0.brep
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for BodyGuard<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Brep {
|
||||
self.0.outward = None;
|
||||
&mut self.0.brep
|
||||
}
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// A body the kernel made.
|
||||
#[must_use]
|
||||
pub(crate) fn new(b: Brep) -> Body {
|
||||
Body(Mutex::new(b))
|
||||
Body::held(b, None)
|
||||
}
|
||||
|
||||
/// A body with the proof [`finish_proved`] made on it, if any.
|
||||
#[must_use]
|
||||
pub(crate) fn held(brep: Brep, outward: Option<Outward>) -> Body {
|
||||
Body(Mutex::new(Held { brep, outward }))
|
||||
}
|
||||
|
||||
/// The kernel's body, locked. A body an earlier panic left unfinished is still handed out;
|
||||
/// [`with_body`] refuses it first.
|
||||
pub(crate) fn lock(&self) -> MutexGuard<'_, Brep> {
|
||||
self.0.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
pub(crate) fn lock(&self) -> BodyGuard<'_> {
|
||||
BodyGuard(self.0.lock().unwrap_or_else(PoisonError::into_inner))
|
||||
}
|
||||
|
||||
/// The kernel's body, locked, unless a panic left it unfinished or it is held now (never
|
||||
/// waited for): why not, if not.
|
||||
pub(crate) fn try_read(&self) -> std::result::Result<MutexGuard<'_, Brep>, &'static str> {
|
||||
self.0.try_lock().map_err(|e| match e {
|
||||
pub(crate) fn try_read(&self) -> std::result::Result<BodyGuard<'_>, &'static str> {
|
||||
self.0.try_lock().map(BodyGuard).map_err(|e| match e {
|
||||
TryLockError::Poisoned(_) => "the body was left unfinished by a panic: not written",
|
||||
TryLockError::WouldBlock => "the body was held: not written",
|
||||
})
|
||||
}
|
||||
|
||||
/// The kernel's body taken out, leaving it empty, with its proof: the one way a proof
|
||||
/// leaves a body (under one lock, before anything can change the B-rep).
|
||||
pub(crate) fn take_held(&self) -> (Brep, Option<Outward>) {
|
||||
let mut held = self.0.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let outward = held.outward.take();
|
||||
(std::mem::replace(&mut held.brep, Brep::new()), outward)
|
||||
}
|
||||
|
||||
/// Whether a panic interrupted an edit of this body.
|
||||
#[must_use]
|
||||
pub(crate) fn is_poisoned(&self) -> bool {
|
||||
@@ -64,7 +164,10 @@ impl Body {
|
||||
#[must_use]
|
||||
#[allow(dead_code)] // the families' operations take their operands out
|
||||
pub(crate) fn into_inner(self) -> Brep {
|
||||
self.0.into_inner().unwrap_or_else(PoisonError::into_inner)
|
||||
self.0
|
||||
.into_inner()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.brep
|
||||
}
|
||||
|
||||
/// A deep copy (`SmApiBrepCopy`): the same topology in the same order, with the attributes
|
||||
@@ -271,6 +374,27 @@ impl Expect {
|
||||
})
|
||||
}
|
||||
|
||||
/// As [`Expect::of`], for an operand taken out of its body with the proof it held
|
||||
/// ([`Body::take_held`]): the manifold test always, then that proof where it stands under
|
||||
/// `k`'s settings, else a new one (`is_outward_solid`'s, step by step). A proof is only
|
||||
/// ever taken in place of the same test passing on the same B-rep: none, or one made under
|
||||
/// other settings, proves as [`Expect::of`] does.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`Expect::of`].
|
||||
pub(crate) fn of_held(
|
||||
k: &mut Kernel,
|
||||
input: &mut Brep,
|
||||
outward: Option<Outward>,
|
||||
) -> solidcore::Result<Expect> {
|
||||
let solid = is_manifold_solid(input)?
|
||||
&& (outward.is_some_and(|o| o.holds(k)) || prove(k, input)?.is_some());
|
||||
Ok(Expect {
|
||||
solid,
|
||||
touching: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// As [`Expect::of`], a solid's result allowed to be solids touching along edges or at
|
||||
/// vertices (closed, `brep_is_closed_touching_solids`), as a boolean's may be.
|
||||
pub(crate) fn touching(self) -> Expect {
|
||||
@@ -304,25 +428,38 @@ impl Expect {
|
||||
///
|
||||
/// # Errors
|
||||
/// The kernel's error from the solid test.
|
||||
pub(crate) fn finish(
|
||||
pub(crate) fn finish(k: &mut Kernel, b: Brep, expect: Expect) -> solidcore::Result<Result<Brep>> {
|
||||
Ok(finish_proved(k, b, expect)?.map(|(b, _)| b))
|
||||
}
|
||||
|
||||
/// As [`finish`], with the proof its solid test made on the result, if it made one (a solid
|
||||
/// result: the topology test, then [`prove`]), for the result's [`Body`] to keep
|
||||
/// ([`Body::held`]).
|
||||
///
|
||||
/// # Errors
|
||||
/// The kernel's error from the solid test.
|
||||
pub(crate) fn finish_proved(
|
||||
k: &mut Kernel,
|
||||
mut b: Brep,
|
||||
expect: Expect,
|
||||
) -> solidcore::Result<Result<Brep>> {
|
||||
) -> solidcore::Result<Result<(Brep, Option<Outward>)>> {
|
||||
if b.face_list().is_empty() {
|
||||
return Ok(Err(Error::Empty));
|
||||
}
|
||||
let solid = |k: &mut Kernel, b: &mut Brep| {
|
||||
if expect.touching {
|
||||
is_outward_touching_solids(k, b)
|
||||
} else {
|
||||
is_outward_solid(k, b)
|
||||
}
|
||||
if !expect.solid {
|
||||
return Ok(Ok((b, None)));
|
||||
}
|
||||
// is_outward_touching_solids / is_outward_solid, step by step, keeping the proof
|
||||
let closed = if expect.touching {
|
||||
is_manifold_solid(&b)? || brep_is_closed_touching_solids(&b)?
|
||||
} else {
|
||||
is_manifold_solid(&b)?
|
||||
};
|
||||
if expect.solid && !solid(k, &mut b)? {
|
||||
let outward = if closed { prove(k, &mut b)? } else { None };
|
||||
if outward.is_none() {
|
||||
return Ok(Err(Error::NotSolid));
|
||||
}
|
||||
Ok(Ok(b))
|
||||
Ok(Ok((b, outward)))
|
||||
}
|
||||
|
||||
/// Copies of each attribute `from` carries in `src`.
|
||||
|
||||
+29
-23
@@ -15,7 +15,7 @@ use solidcore::primitive::primitive_creation::p1::Boolean2d;
|
||||
use solidcore::Brep;
|
||||
use solidcore::Result as KernelResult;
|
||||
|
||||
use crate::body::{finish, Body, Expect};
|
||||
use crate::body::{finish_proved, Body, Expect, Outward};
|
||||
use crate::session::run_taking;
|
||||
use crate::{Error, Result};
|
||||
|
||||
@@ -32,7 +32,8 @@ pub enum BooleanOp {
|
||||
|
||||
/// What a call gave before it is a result.
|
||||
enum Done {
|
||||
Body(Box<Brep>),
|
||||
/// The result, with the proof [`finish_proved`] made on it.
|
||||
Body(Box<Brep>, Option<Outward>),
|
||||
Empty,
|
||||
Refused(Error),
|
||||
}
|
||||
@@ -46,21 +47,25 @@ impl crate::session::Answer for Done {
|
||||
}
|
||||
}
|
||||
|
||||
/// The result contract of a boolean ([`finish`]): faces left, and a solid in gives a solid out,
|
||||
/// solids touching along an edge or at a vertex allowed.
|
||||
/// The result contract of a boolean ([`finish_proved`]): faces left, and a solid in gives a
|
||||
/// solid out, solids touching along an edge or at a vertex allowed; the result keeps the proof
|
||||
/// its solid test made, for the next operation that takes it as an operand.
|
||||
fn finished(k: &mut Kernel, r: Brep, expect: Expect) -> KernelResult<Done> {
|
||||
Ok(match finish(k, r, expect.touching())? {
|
||||
Ok(b) => Done::Body(Box::new(b)),
|
||||
Ok(match finish_proved(k, r, expect.touching())? {
|
||||
Ok((b, outward)) => Done::Body(Box::new(b), outward),
|
||||
Err(Error::Empty) => Done::Empty,
|
||||
Err(e) => Done::Refused(e),
|
||||
})
|
||||
}
|
||||
|
||||
/// What the operands make a boolean's result: a solid when every operand is one.
|
||||
fn expect_of(k: &mut Kernel, operands: &mut [Brep]) -> KernelResult<Expect> {
|
||||
/// What the operands make a boolean's result: a solid when every operand is one. Each comes
|
||||
/// with the proof its body held ([`Body::take_held`]), taken where it stands
|
||||
/// ([`Expect::of_held`]): an operand a previous operation proved, unchanged since, is not
|
||||
/// proved again (#14830).
|
||||
fn expect_of(k: &mut Kernel, operands: &mut [(Brep, Option<Outward>)]) -> KernelResult<Expect> {
|
||||
let mut all = true;
|
||||
for b in operands.iter_mut() {
|
||||
all &= Expect::of(k, b)? != Expect::FACES;
|
||||
for (b, outward) in operands.iter_mut() {
|
||||
all &= Expect::of_held(k, b, outward.take())? != Expect::FACES;
|
||||
}
|
||||
Ok(if all { Expect::SOLID } else { Expect::FACES })
|
||||
}
|
||||
@@ -86,9 +91,9 @@ fn usable(call: &'static str, bodies: &[Body]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The kernel's body taken out of `b`, leaving it empty.
|
||||
fn take(b: &Body) -> Brep {
|
||||
std::mem::replace(&mut *b.lock(), Brep::new())
|
||||
/// The kernel's body taken out of `b`, leaving it empty, with the proof it held.
|
||||
fn take(b: &Body) -> (Brep, Option<Outward>) {
|
||||
b.take_held()
|
||||
}
|
||||
|
||||
fn operation(op: BooleanOp) -> BooleanOperation {
|
||||
@@ -101,7 +106,7 @@ fn operation(op: BooleanOp) -> BooleanOperation {
|
||||
|
||||
fn result(d: Done, empty: Result<Option<Body>>) -> Result<Option<Body>> {
|
||||
match d {
|
||||
Done::Body(b) => Ok(Some(Body::new(*b))),
|
||||
Done::Body(b, outward) => Ok(Some(Body::held(*b, outward))),
|
||||
Done::Empty => empty,
|
||||
Done::Refused(e) => Err(e),
|
||||
}
|
||||
@@ -120,7 +125,7 @@ pub fn boolean(a: Body, b: Body, op: BooleanOp) -> Result<Option<Body>> {
|
||||
let [a, b] = &both;
|
||||
let mut operands = [take(a), take(b)];
|
||||
let expect = expect_of(k, &mut operands)?;
|
||||
let [ca, cb] = operands;
|
||||
let [(ca, _), (cb, _)] = operands;
|
||||
let r = sm::boolean(k, ca, cb, operation(op))?;
|
||||
finished(k, r.result, expect)
|
||||
})?;
|
||||
@@ -141,8 +146,9 @@ pub fn union_all(bodies: Vec<Body>) -> Result<Body> {
|
||||
}
|
||||
usable("union_all", &bodies)?;
|
||||
let d = run_taking("union_all", &operands(&bodies), |k| {
|
||||
let mut level: Vec<Brep> = bodies.iter().map(take).collect();
|
||||
let expect = expect_of(k, &mut level)?;
|
||||
let mut held: Vec<(Brep, Option<Outward>)> = bodies.iter().map(take).collect();
|
||||
let expect = expect_of(k, &mut held)?;
|
||||
let mut level: Vec<Brep> = held.into_iter().map(|(b, _)| b).collect();
|
||||
while level.len() > 1 {
|
||||
let mut next = Vec::new();
|
||||
let mut it = level.into_iter();
|
||||
@@ -172,7 +178,7 @@ pub fn boolean2d(a: Body, b: Body, op: BooleanOp) -> Result<Option<Body>> {
|
||||
usable("SmApiBoolean2d", &both)?;
|
||||
let d = run_taking("SmApiBoolean2d", &operands(&both), |k| {
|
||||
let [a, b] = &both;
|
||||
let (ca, cb) = (take(a), take(b));
|
||||
let ((ca, _), (cb, _)) = (take(a), take(b));
|
||||
if ca.face_list().is_empty() || cb.face_list().is_empty() {
|
||||
return Ok(Done::Empty);
|
||||
}
|
||||
@@ -200,7 +206,7 @@ fn counts(b: &Brep) -> [usize; 3] {
|
||||
|
||||
/// `tool`'s edges imprinted on `body` (`SmApiBooleanWithOptions(SM_BO_IMPRINT)`: cookie cutter
|
||||
/// off, imprinting on, post-process off, imprint-and-classify off, keep other on), with the
|
||||
/// result contract ([`finish`]: faces left, a solid in gives a solid out), and whether the
|
||||
/// result contract ([`finish_proved`]: faces left, a solid in gives a solid out), and whether the
|
||||
/// imprint changed `body`'s topology (a face cut, an edge split: its face, edge or vertex
|
||||
/// count moved). When it changed nothing the body comes back as the kernel left it, never
|
||||
/// dropped.
|
||||
@@ -214,17 +220,17 @@ pub fn imprint(body: Body, tool: Body) -> Result<(Body, bool)> {
|
||||
let mut before = [0; 3];
|
||||
let d = run_taking("SmApiBooleanWithOptions", &operands(&both), |k| {
|
||||
let [body, tool] = &both;
|
||||
let (mut a, t) = (take(body), take(tool));
|
||||
let ((mut a, outward), (t, _)) = (take(body), take(tool));
|
||||
before = counts(&a);
|
||||
let expect = Expect::of(k, &mut a)?;
|
||||
let expect = Expect::of_held(k, &mut a, outward)?;
|
||||
let options = MergeOptions::new(false, true, false, false, true);
|
||||
let r = sm::boolean_with_options(k, a, t, BooleanOperation::Imprint, options)?.result;
|
||||
finished(k, r, expect)
|
||||
})?;
|
||||
match d {
|
||||
Done::Body(b) => {
|
||||
Done::Body(b, outward) => {
|
||||
let changed = counts(&b) != before;
|
||||
Ok((Body::new(*b), changed))
|
||||
Ok((Body::held(*b, outward), changed))
|
||||
}
|
||||
Done::Empty => Err(Error::Empty),
|
||||
Done::Refused(e) => Err(e),
|
||||
|
||||
+79
-1
@@ -37,7 +37,10 @@ pub fn cv_curve(points: &[[f64; 3]], degree: usize) -> Result<Curve> {
|
||||
}
|
||||
|
||||
/// `curve` with its control point `index` at `to`: its knots, degree and every other point as
|
||||
/// they were. A spline's or a polyline's point.
|
||||
/// they were. A spline's or a polyline's point. On a closed periodic spline (unclamped knots, its
|
||||
/// polygon repeating its first `degree` points after its last, as
|
||||
/// [`crate::curve_fit::closed_cv_curve`] makes it) a repeated point moves with its copy, so the
|
||||
/// curve stays closed and smooth.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for another kind of curve (a rational spline's point carries its weight),
|
||||
@@ -46,14 +49,42 @@ pub fn move_cv(curve: &Curve, index: usize, to: [f64; 3]) -> Result<Curve> {
|
||||
if !coord_ok(&to) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let twin = match curve {
|
||||
Curve::Spline { knots, points } => periodic_twin(knots, points, index),
|
||||
_ => None,
|
||||
};
|
||||
let mut moved = curve.clone();
|
||||
let (Curve::Spline { points, .. } | Curve::Polyline { points }) = &mut moved else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
*points.get_mut(index).ok_or(Error::Argument)? = to;
|
||||
if let Some(copy) = twin {
|
||||
*points.get_mut(copy).ok_or(Error::Argument)? = to;
|
||||
}
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
/// The index of the copy of control point `index` of a closed periodic spline: one whose knots
|
||||
/// are periodic ([`solidcore::nurbs::knots::is_periodic`]) and whose last `degree` points
|
||||
/// repeat its first `degree`. `None` for another spline, or a point that is not repeated.
|
||||
fn periodic_twin(knots: &[f64], points: &[[f64; 3]], index: usize) -> Option<usize> {
|
||||
let degree = knots.len().checked_sub(points.len() + 1)?;
|
||||
let distinct = points.len().checked_sub(degree)?;
|
||||
if !solidcore::nurbs::knots::is_periodic(knots, degree) {
|
||||
return None;
|
||||
}
|
||||
let repeats =
|
||||
(0..degree).all(|i| points.get(i).is_some() && points.get(i) == points.get(i + distinct));
|
||||
if !repeats {
|
||||
return None;
|
||||
}
|
||||
if index < degree {
|
||||
Some(index + distinct)
|
||||
} else {
|
||||
index.checked_sub(distinct).filter(|i| *i < degree)
|
||||
}
|
||||
}
|
||||
|
||||
/// The curvature comb of `curve` over `interval` of its parameter: the kernel's curvature vector
|
||||
/// at `teeth` even steps, both ends included. Stations where the curve has no tangent are left
|
||||
/// out.
|
||||
@@ -179,6 +210,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_a_repeated_cv_of_a_closed_curve_moves_its_copy() {
|
||||
let c = crate::curve_fit::closed_cv_curve(&SQUARE, 3).unwrap();
|
||||
// points 0, 1 and 2 are repeated as 4, 5 and 6
|
||||
for (index, copy) in [(1, 5), (5, 1), (0, 4), (6, 2)] {
|
||||
let moved = move_cv(&c, index, [5.0, -3.0, 2.0]).unwrap();
|
||||
let Curve::Spline { points, knots } = &moved else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(points[index], [5.0, -3.0, 2.0]);
|
||||
assert_eq!(points[copy], [5.0, -3.0, 2.0]);
|
||||
let Curve::Spline { knots: k0, .. } = &c else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(knots, k0);
|
||||
}
|
||||
// the one point not repeated moves alone
|
||||
let moved = move_cv(&c, 3, [5.0, -3.0, 2.0]).unwrap();
|
||||
let (Curve::Spline { points: p0, .. }, Curve::Spline { points: p1, .. }) = (&c, &moved)
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
for i in [0, 1, 2, 4, 5, 6] {
|
||||
assert_eq!(p0[i], p1[i]);
|
||||
}
|
||||
// repeated points on unclamped knots that are not periodic move alone
|
||||
let (a, b, c) = ([0.0; 3], [4.0, 0.0, 0.0], [4.0, 4.0, 0.0]);
|
||||
let open = Curve::Spline {
|
||||
knots: vec![-2.0, -1.0, 0.0, 1.0, 2.0, 4.0, 5.0, 6.0],
|
||||
points: vec![a, b, c, a, b],
|
||||
};
|
||||
let moved = move_cv(&open, 0, [1.0, 1.0, 1.0]).unwrap();
|
||||
let Curve::Spline { points, .. } = &moved else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!((points[0], points[3]), ([1.0, 1.0, 1.0], a));
|
||||
// a clamped curve whose last point is its first is not periodic
|
||||
let mut ring = SQUARE.to_vec();
|
||||
ring.push(SQUARE[0]);
|
||||
let open = cv_curve(&ring, 3).unwrap();
|
||||
let moved = move_cv(&open, 0, [1.0, 1.0, 1.0]).unwrap();
|
||||
let Curve::Spline { points, .. } = &moved else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(points[4], SQUARE[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_arcs_comb_points_at_its_centre_with_the_inverse_radius() {
|
||||
// A quarter circle of radius 10 about (1, 2, 3) in its z plane, as a rational quadratic.
|
||||
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
//! Curves fitted to points: through points (edit points), closed, and rebuilt to a degree and span count.
|
||||
//! The curve is the record's ([`Curve`]); the knots, the fits and the bound are the kernel's
|
||||
//! ([`interp`]: global interpolation, The NURBS Book's §9.2.1; [`periodic_uniform`];
|
||||
//! [`approx_knots`]: least squares at given parameters, §9.4.1; [`certified_distance`] and
|
||||
//! [`interpolation_bound`]). Ops checks the inputs and converts; every fit comes with the
|
||||
//! kernel's certified bound of how far it is from what it fits.
|
||||
|
||||
use solidcore::nurbs::crv_deviation::{certified_distance, interpolation_bound};
|
||||
use solidcore::nurbs::crv_fit::p1::{approx_knots, param_values, ParamMethod};
|
||||
use solidcore::nurbs::crv_fit::p2::interp;
|
||||
use solidcore::nurbs::geom::Interval;
|
||||
use solidcore::nurbs::knots::{clamped_uniform, periodic_uniform, KnotVector, Side, MAX_DEGREE};
|
||||
use solidcore::Vec3;
|
||||
use solidcore_record::brep::{check_curve, Budget, Curve, MAX_SPLINE_DEGREE};
|
||||
|
||||
use crate::curve_knots::{nurb_of, record_of};
|
||||
use crate::session::run;
|
||||
use crate::sweep::{coord_ok, CURVE_MAX_POINTS};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The clamped B-spline of `degree` through `points` in order, on `[0, 1]`, each point at its
|
||||
/// share of the polygon's length (chord-length parameters) and the knots their averages (The
|
||||
/// NURBS Book, eq. 9.8), and the kernel's certified bound of how far the curve misses a point
|
||||
/// at its parameter ([`interpolation_bound`]): interpolation in floating point is not exact.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for fewer than two points, more than [`CURVE_MAX_POINTS`] (or than a
|
||||
/// record holds), a coordinate that is not finite or past the coordinate bound, a point
|
||||
/// repeating the one before it (a chord of no length), or a degree of 0, not below the number
|
||||
/// of points, or past the kernel's most; the kernel's status otherwise (points so close the fit
|
||||
/// is singular).
|
||||
pub fn ep_curve(points: &[[f64; 3]], degree: usize) -> Result<(Curve, f64)> {
|
||||
if points.len() < 2 || points.len() > CURVE_MAX_POINTS || !points.iter().all(|p| coord_ok(p)) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if points.windows(2).any(|w| w.first() == w.get(1)) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if degree == 0 || degree >= points.len() || degree > MAX_DEGREE {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let data: Vec<Vec3> = points.iter().map(|&[x, y, z]| Vec3::new(x, y, z)).collect();
|
||||
run("curve_fit::ep_curve", |_| {
|
||||
let params = param_values(&data, ParamMethod::ChordLength)?;
|
||||
let fit = interp(&data, degree, ParamMethod::ChordLength)?;
|
||||
let bound = interpolation_bound(&fit, ¶ms, &data)?;
|
||||
Ok(record_of(&fit).map(|c| (c, bound)))
|
||||
})?
|
||||
}
|
||||
|
||||
/// The closed B-spline of `degree` (below the number of points) with `points`
|
||||
/// as its control polygon, closing from the last back to the first, on `[0, 1]`: periodic,
|
||||
/// so it joins itself smoothly (continuity `degree - 1`) and passes through none of its
|
||||
/// points. The record holds it as a spline whose polygon repeats its first `degree` points
|
||||
/// after its last, on the kernel's unclamped uniform knots ([`periodic_uniform`]);
|
||||
/// [`crate::curve::move_cv`] moves a repeated point with its copy.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for fewer than three points, more than [`CURVE_MAX_POINTS`] (or than a
|
||||
/// record holds), a coordinate that is not finite or past the coordinate bound, or a degree
|
||||
/// of 0, not below the number of points, or past the kernel's most.
|
||||
pub fn closed_cv_curve(points: &[[f64; 3]], degree: usize) -> Result<Curve> {
|
||||
if points.len() < 3 || points.len() > CURVE_MAX_POINTS || !points.iter().all(|p| coord_ok(p)) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if degree == 0 || degree >= points.len() || degree > MAX_DEGREE {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let knots = periodic_uniform(points.len(), degree).map_err(|_| Error::Argument)?;
|
||||
let mut wrapped = points.to_vec();
|
||||
wrapped.extend_from_slice(points.get(..degree).ok_or(Error::Argument)?);
|
||||
let curve = Curve::Spline {
|
||||
knots,
|
||||
points: wrapped,
|
||||
};
|
||||
check_curve(&curve, &mut Budget::input()).map_err(|_| Error::Argument)?;
|
||||
Ok(curve)
|
||||
}
|
||||
|
||||
/// `curve` refitted as a clamped spline of `degree` with `spans` uniform spans on `[0, 1]`,
|
||||
/// and the kernel's certified bound of how far it is from `curve` at every parameter
|
||||
/// ([`certified_distance`]): `curve` is sampled at even steps of its parameter (mapped onto
|
||||
/// `[0, 1]`), four per control point, and the samples fitted by least squares at those
|
||||
/// parameters through its two ends ([`approx_knots`]).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a curve that is not a valid clamped spline (a rational spline,
|
||||
/// whose bound the kernel does not certify yet, and a closed periodic one are refused), a
|
||||
/// degree of 0 or past the record's most ([`MAX_SPLINE_DEGREE`]), no spans, fewer than three
|
||||
/// control points (`spans + degree`) or more than a record holds; the kernel's status
|
||||
/// otherwise.
|
||||
pub fn rebuild(curve: &Curve, degree: usize, spans: usize) -> Result<(Curve, f64)> {
|
||||
let nurb = nurb_of(curve)?;
|
||||
if !matches!(curve, Curve::Spline { .. })
|
||||
|| degree == 0
|
||||
|| degree > MAX_SPLINE_DEGREE
|
||||
|| spans == 0
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let count = spans.checked_add(degree).ok_or(Error::Argument)?;
|
||||
if !(3..=4096).contains(&count) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let samples = count
|
||||
.checked_mul(4)
|
||||
.and_then(|s| s.checked_add(1))
|
||||
.ok_or(Error::Argument)?;
|
||||
// even steps on [0, 1]: the distinct knots of a polyline of that many points
|
||||
let steps = clamped_uniform(samples, 1).map_err(|_| Error::Argument)?;
|
||||
let params = steps.get(1..=samples).ok_or(Error::Argument)?.to_vec();
|
||||
let knots = clamped_uniform(count, degree).map_err(|_| Error::Argument)?;
|
||||
run("curve_fit::rebuild", |_| {
|
||||
let mut unit = nurb.clone();
|
||||
unit.reparam_to_interval(Interval { ul: 0.0, ur: 1.0 })?;
|
||||
let data = params
|
||||
.iter()
|
||||
.map(|&t| unit.eval(t, Side::Left))
|
||||
.collect::<solidcore::Result<Vec<Vec3>>>()?;
|
||||
let fit = approx_knots(
|
||||
&data,
|
||||
¶ms,
|
||||
&KnotVector::from_slice(&knots)?,
|
||||
count - 1,
|
||||
degree,
|
||||
)?;
|
||||
let bound = certified_distance(&unit, &fit)?;
|
||||
Ok(record_of(&fit).map(|c| (c, bound)))
|
||||
})?
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::curve::{curve_comb, cv_curve};
|
||||
use solidcore::nurbs::crv_basic::Curve as NurbCurve;
|
||||
use solidcore::nurbs::math::{CPoint, NOW};
|
||||
|
||||
/// The point of `c` at `t`, by the kernel's evaluator.
|
||||
fn at(c: &Curve, t: f64) -> [f64; 3] {
|
||||
let p = nurb_of(c).unwrap().eval(t, Side::Left).unwrap();
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
fn passes_through(c: &Curve, points: &[[f64; 3]], params: &[f64]) {
|
||||
for (p, t) in points.iter().zip(params) {
|
||||
let q = at(c, *t);
|
||||
assert!(
|
||||
p.iter().zip(q).all(|(a, b)| (a - b).abs() < 1e-12),
|
||||
"at {t}: {q:?}, not {p:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cubic_passes_through_its_points_at_their_share_of_the_length() {
|
||||
// Chords of 5, 12 and 5: the points at 0, 5/22, 17/22 and 1 (uniform parameters would
|
||||
// put the second at 1/3).
|
||||
let points = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[3.0, 4.0, 0.0],
|
||||
[3.0, 4.0, 12.0],
|
||||
[6.0, 8.0, 12.0],
|
||||
];
|
||||
let (c, bound) = ep_curve(&points, 3).unwrap();
|
||||
assert!(bound < 1e-12, "{bound}");
|
||||
let Curve::Spline { knots, points: cvs } = &c else {
|
||||
panic!("a spline: {c:?}")
|
||||
};
|
||||
assert_eq!(knots, &vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
|
||||
assert_eq!(cvs.len(), 4);
|
||||
passes_through(&c, &points, &[0.0, 5.0 / 22.0, 17.0 / 22.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_points_than_the_degree_make_interior_knots() {
|
||||
// Six points with chords of 5: parameters i / 5, interior knots the averages of three.
|
||||
let points = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[3.0, 4.0, 0.0],
|
||||
[6.0, 0.0, 0.0],
|
||||
[9.0, 4.0, 0.0],
|
||||
[12.0, 0.0, 0.0],
|
||||
[15.0, 4.0, 0.0],
|
||||
];
|
||||
let params: Vec<f64> = (0..6).map(|i| f64::from(i) / 5.0).collect();
|
||||
let (c, bound) = ep_curve(&points, 3).unwrap();
|
||||
assert!(bound < 1e-12, "{bound}");
|
||||
let Curve::Spline { knots, points: cvs } = &c else {
|
||||
panic!("a spline: {c:?}")
|
||||
};
|
||||
assert_eq!(cvs.len(), 6);
|
||||
assert_eq!(knots.len(), 10);
|
||||
for (k, want) in knots[4..6].iter().zip([0.4, 0.6]) {
|
||||
assert!((k - want).abs() < 1e-12, "{knots:?}");
|
||||
}
|
||||
passes_through(&c, &points, ¶ms);
|
||||
|
||||
let (quadratic, _) = ep_curve(&points, 2).unwrap();
|
||||
passes_through(&quadratic, &points, ¶ms);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_points_make_a_line() {
|
||||
let ends = [[1.0, 2.0, 3.0], [4.0, 6.0, 3.0]];
|
||||
let (line, bound) = ep_curve(&ends, 1).unwrap();
|
||||
assert_eq!(
|
||||
line,
|
||||
Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 1.0, 1.0],
|
||||
points: ends.to_vec(),
|
||||
}
|
||||
);
|
||||
assert!(bound < 1e-12, "{bound}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_points_are_refused() {
|
||||
let ends = [[0.0; 3], [1.0, 0.0, 0.0]];
|
||||
assert_eq!(ep_curve(&ends[..1], 3), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
ep_curve(&[[0.0; 3], [f64::NAN, 0.0, 0.0]], 1),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
ep_curve(&[[0.0; 3], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]], 2),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(ep_curve(&ends, 0), Err(Error::Argument));
|
||||
// a degree not below the number of points is refused, not lowered
|
||||
assert_eq!(ep_curve(&ends, 2), Err(Error::Argument));
|
||||
assert_eq!(ep_curve(&ends[..], 3), Err(Error::Argument));
|
||||
assert_eq!(ep_curve(&ends, MAX_DEGREE + 1), Err(Error::Argument));
|
||||
}
|
||||
|
||||
const SQUARE: [[f64; 3]; 4] = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
];
|
||||
|
||||
/// The kernel's curve of a spline record as it is, clamped or not.
|
||||
fn unclamped(c: &Curve) -> NurbCurve {
|
||||
let Curve::Spline { knots, points } = c else {
|
||||
panic!("a spline: {c:?}")
|
||||
};
|
||||
let pw = points
|
||||
.iter()
|
||||
.map(|&[x, y, z]| CPoint::new(x, y, z, NOW))
|
||||
.collect();
|
||||
NurbCurve::new(pw, knots.len() - points.len() - 1, knots.clone()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_curve_joins_itself_smoothly() {
|
||||
let c = closed_cv_curve(&SQUARE, 3).unwrap();
|
||||
let Curve::Spline { knots, points } = &c else {
|
||||
panic!("a spline: {c:?}")
|
||||
};
|
||||
assert_eq!(points.len(), 7);
|
||||
assert_eq!(&points[4..], &SQUARE[..3]);
|
||||
assert_eq!(knots.len(), 11);
|
||||
assert_eq!((knots[3], knots[7]), (0.0, 1.0));
|
||||
let k = unclamped(&c);
|
||||
// the uniform cubic starts at (P0 + 4 P1 + P2) / 6 (the basis at a knot of a uniform
|
||||
// cubic is 1/6, 4/6, 1/6), and ends there: read through `derivs`, the general basis
|
||||
// (the NLib `eval` keeps upstream's clamped reading of the domain's ends)
|
||||
let start = k.derivs(0.0, Side::Right, 0).unwrap()[0];
|
||||
let end = k.derivs(1.0, Side::Left, 0).unwrap()[0];
|
||||
assert!((start.x - 50.0 / 6.0).abs() < 1e-12 && (start.y - 10.0 / 6.0).abs() < 1e-12);
|
||||
assert!((end.x - 50.0 / 6.0).abs() < 1e-12 && (end.y - 10.0 / 6.0).abs() < 1e-12);
|
||||
// position, first and second derivatives meet across the seam
|
||||
let (a, b) = (
|
||||
k.derivs(0.0, Side::Right, 2).unwrap(),
|
||||
k.derivs(1.0, Side::Left, 2).unwrap(),
|
||||
);
|
||||
for (x, y) in a.iter().zip(&b) {
|
||||
assert!(
|
||||
(x.x - y.x).abs() < 1e-9 && (x.y - y.y).abs() < 1e-9 && (x.z - y.z).abs() < 1e-9,
|
||||
"{a:?} and {b:?}"
|
||||
);
|
||||
}
|
||||
// the kernel takes it as an edge's curve: its comb's end teeth stand on one point
|
||||
let comb = curve_comb(&c, [0.0, 1.0], 9).unwrap();
|
||||
let (first, last) = (comb.first().unwrap(), comb.last().unwrap());
|
||||
assert!(
|
||||
first
|
||||
.at
|
||||
.iter()
|
||||
.zip(last.at)
|
||||
.all(|(x, y)| (x - y).abs() < 1e-9),
|
||||
"{:?} and {:?}",
|
||||
first.at,
|
||||
last.at
|
||||
);
|
||||
// a degree not below the number of points is refused, not lowered; two points make no
|
||||
// closed curve
|
||||
let Curve::Spline { knots, points } = closed_cv_curve(&SQUARE[..3], 2).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(knots.len() - points.len() - 1, 2);
|
||||
assert_eq!(closed_cv_curve(&SQUARE[..3], 3), Err(Error::Argument));
|
||||
assert_eq!(closed_cv_curve(&SQUARE[..2], 1), Err(Error::Argument));
|
||||
assert_eq!(closed_cv_curve(&SQUARE, 0), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_curve_of_degree_p_is_c_p_minus_1_across_its_seam() {
|
||||
let hexagon = [
|
||||
[10.0, 0.0, 0.0],
|
||||
[5.0, 8.0, 1.0],
|
||||
[-5.0, 8.0, 0.0],
|
||||
[-10.0, 0.0, -1.0],
|
||||
[-5.0, -8.0, 0.0],
|
||||
[5.0, -8.0, 2.0],
|
||||
];
|
||||
for p in 1..=5 {
|
||||
let k = unclamped(&closed_cv_curve(&hexagon, p).unwrap());
|
||||
// every derivative of order below p meets across the seam
|
||||
let (a, b) = (
|
||||
k.derivs(0.0, Side::Right, p - 1).unwrap(),
|
||||
k.derivs(1.0, Side::Left, p - 1).unwrap(),
|
||||
);
|
||||
assert_eq!(a.len(), p);
|
||||
for (order, (x, y)) in a.iter().zip(&b).enumerate() {
|
||||
for (u, v) in [(x.x, y.x), (x.y, y.y), (x.z, y.z)] {
|
||||
assert!(
|
||||
(u - v).abs() <= 1e-9 * (1.0 + u.abs()),
|
||||
"degree {p}, derivative {order}: {x:?} and {y:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WAVE: [[f64; 3]; 6] = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
[2.0, -1.0, 0.0],
|
||||
[3.0, 3.0, 0.0],
|
||||
[4.0, 0.0, 1.0],
|
||||
[5.0, 1.0, 0.0],
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn a_curve_rebuilt_in_its_own_space_is_itself() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let (r, bound) = rebuild(&c, 3, 3).unwrap();
|
||||
assert!(bound < 1e-9, "{bound}");
|
||||
let (Curve::Spline { knots: k0, .. }, Curve::Spline { knots: k1, .. }) = (&c, &r) else {
|
||||
panic!("splines")
|
||||
};
|
||||
assert_eq!(k0, k1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rebuilt_curves_bound_holds_its_gap_everywhere() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let (r, bound) = rebuild(&c, 3, 1).unwrap();
|
||||
let Curve::Spline { knots, points } = &r else {
|
||||
panic!("a spline: {r:?}")
|
||||
};
|
||||
assert_eq!(points.len(), 4);
|
||||
assert_eq!(knots, &vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
|
||||
assert!(bound > 0.0);
|
||||
let mut largest = 0.0_f64;
|
||||
for i in 0..=400 {
|
||||
let t = f64::from(i) / 400.0;
|
||||
let (p, q) = (at(&c, t), at(&r, t));
|
||||
let gap2: f64 = p.iter().zip(q).map(|(a, b)| (a - b) * (a - b)).sum();
|
||||
largest = largest.max(gap2);
|
||||
}
|
||||
assert!(largest <= bound * bound, "{largest} past {bound}²");
|
||||
// the ends stay put
|
||||
for (t, end) in [(0.0, WAVE[0]), (1.0, WAVE[5])] {
|
||||
let p = at(&r, t);
|
||||
assert!(
|
||||
p.iter().zip(end).all(|(a, b)| (a - b).abs() < 1e-12),
|
||||
"{p:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_curve_on_another_domain_is_rebuilt_on_zero_to_one() {
|
||||
let c = Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 2.0],
|
||||
points: WAVE[..4].to_vec(),
|
||||
};
|
||||
let (r, bound) = rebuild(&c, 3, 1).unwrap();
|
||||
assert!(bound < 1e-9, "{bound}");
|
||||
assert_eq!(
|
||||
r.natural(),
|
||||
solidcore_record::brep::Natural::Range(0.0, 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rebuild_with_bad_arguments_is_refused() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
assert_eq!(rebuild(&c, 0, 3), Err(Error::Argument));
|
||||
assert_eq!(rebuild(&c, 3, 0), Err(Error::Argument));
|
||||
assert_eq!(rebuild(&c, 1, 1), Err(Error::Argument));
|
||||
assert_eq!(rebuild(&c, MAX_SPLINE_DEGREE + 1, 1), Err(Error::Argument));
|
||||
let closed = closed_cv_curve(&SQUARE, 3).unwrap();
|
||||
assert_eq!(rebuild(&closed, 3, 3), Err(Error::Argument));
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let arc = Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[10.0, 0.0, 0.0, 1.0],
|
||||
[10.0 * w, 10.0 * w, 0.0, w],
|
||||
[0.0, 10.0, 0.0, 1.0],
|
||||
],
|
||||
};
|
||||
assert_eq!(rebuild(&arc, 3, 2), Err(Error::Argument));
|
||||
}
|
||||
}
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
//! Knots and degree of a free curve: a knot inserted or removed, the degree raised or lowered.
|
||||
//! The curve is the record's ([`Curve`], a clamped spline or rational spline); every step is the
|
||||
//! kernel's ([`NurbCurve::insert_knot`], [`NurbCurve::remove_knot`],
|
||||
//! [`NurbCurve::elevate_degree`], [`NurbCurve::reduce_degree_once`]: The NURBS Book's algorithms
|
||||
//! A5.1, A5.8, A5.9 and A5.11), and a change that is not exact is kept only within a tolerance
|
||||
//! of the kernel's certified bound ([`certified_distance`]). Ops checks the inputs and converts.
|
||||
//! A closed periodic curve (unclamped knots, as `curve_fit::closed_cv_curve` makes) is refused
|
||||
//! for now: the kernel's knot routines take clamped knots.
|
||||
|
||||
use solidcore::nurbs::crv_basic::Curve as NurbCurve;
|
||||
use solidcore::nurbs::crv_deviation::certified_distance;
|
||||
use solidcore::nurbs::math::{CPoint, NOW};
|
||||
use solidcore_record::brep::{check_curve, Budget, Curve, Natural, MAX_SPLINE_DEGREE};
|
||||
|
||||
use crate::session::run;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// `curve` with the knot `t` inserted once: the same curve, one control point more.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a curve that is not a valid clamped spline or rational spline, a `t`
|
||||
/// not strictly inside its domain, or a knot already as many times as the degree (one more would
|
||||
/// break the curve); the kernel's status otherwise.
|
||||
pub fn insert_knot(curve: &Curve, t: f64) -> Result<Curve> {
|
||||
let nurb = nurb_of(curve)?;
|
||||
if !interior(curve, t) || multiplicity(nurb.knots(), t) >= nurb.degree() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("curve_knots::insert_knot", |_| {
|
||||
Ok(record_of(&nurb.insert_knot(t, 1)?))
|
||||
})?
|
||||
}
|
||||
|
||||
/// `curve` with one copy of its interior knot `t` removed (The NURBS Book, A5.8; a knot of
|
||||
/// multiplicity `m` keeps `m - 1`), and the kernel's certified bound of how far the result is
|
||||
/// from `curve` ([`solidcore::nurbs::crv_deviation::certified_distance`], measured against
|
||||
/// the curve as it was, never the removal's own estimate), kept when within `tol`.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a curve that is not a valid clamped spline (a rational spline, whose
|
||||
/// bound the kernel does not certify yet, and a closed periodic one are refused), a `t` that is
|
||||
/// not one of its interior knots, or a `tol` not finite or negative;
|
||||
/// [`Error::PastTolerance`] with the bound when it is past `tol`; the kernel's status otherwise.
|
||||
pub fn remove_knot(curve: &Curve, t: f64, tol: f64) -> Result<(Curve, f64)> {
|
||||
let nurb = nurb_of(curve)?;
|
||||
if !matches!(curve, Curve::Spline { .. })
|
||||
|| !interior(curve, t)
|
||||
|| multiplicity(nurb.knots(), t) == 0
|
||||
|| !tol.is_finite()
|
||||
|| tol < 0.0
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("curve_knots::remove_knot", |_| {
|
||||
// removed whatever it moves; the certified bound decides
|
||||
let removal = nurb.remove_knot(t, 1, f64::MAX)?;
|
||||
if removal.removed == 0 {
|
||||
return Ok(Err(Error::NoEffect));
|
||||
}
|
||||
within(&nurb, &removal.curve, tol)
|
||||
})?
|
||||
}
|
||||
|
||||
/// `curve` of degree `degree`, and the kernel's certified bound of how far it is from `curve`
|
||||
/// ([`solidcore::nurbs::crv_deviation::certified_distance`]). Raising it is exact in exact
|
||||
/// arithmetic (the same curve, more control points; `tol` is not used), its bound the
|
||||
/// rounding; the same degree is the curve as it is, bound 0; lowering it (The NURBS Book,
|
||||
/// A5.11, once per degree) is kept when the bound is within `tol`. A rational spline is raised
|
||||
/// with no bound (`None`): the kernel's bound does not take rational curves yet.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a curve that is not a valid clamped spline or rational spline (a
|
||||
/// closed periodic one is refused), a degree of 0 or past the record's most
|
||||
/// ([`MAX_SPLINE_DEGREE`]), a `tol` not finite or negative, or a lower degree of a rational
|
||||
/// spline (whose bound the kernel does not certify yet); [`Error::PastTolerance`] with the
|
||||
/// bound when it is past `tol`; [`Error::NoEffect`] when the kernel does not lower it; the
|
||||
/// kernel's status otherwise.
|
||||
pub fn set_degree(curve: &Curve, degree: usize, tol: f64) -> Result<(Curve, Option<f64>)> {
|
||||
let nurb = nurb_of(curve)?;
|
||||
if degree == 0 || degree > MAX_SPLINE_DEGREE || !tol.is_finite() || tol < 0.0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if let Some(raise) = degree.checked_sub(nurb.degree()) {
|
||||
if raise == 0 {
|
||||
return Ok((curve.clone(), Some(0.0)));
|
||||
}
|
||||
let rational = !matches!(curve, Curve::Spline { .. });
|
||||
return run("curve_knots::set_degree", |_| {
|
||||
let raised = nurb.elevate_degree(raise)?;
|
||||
if rational {
|
||||
return Ok(record_of(&raised).map(|c| (c, None)));
|
||||
}
|
||||
let bound = certified_distance(&nurb, &raised)?;
|
||||
Ok(record_of(&raised).map(|c| (c, Some(bound))))
|
||||
})?;
|
||||
}
|
||||
if !matches!(curve, Curve::Spline { .. }) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
run("curve_knots::set_degree", |_| {
|
||||
let mut lowered = nurb.clone();
|
||||
while lowered.degree() > degree {
|
||||
// lowered whatever it moves; the certified bound decides
|
||||
match lowered.reduce_degree_once(f64::MAX)?.curve {
|
||||
Some(c) => lowered = c,
|
||||
None => return Ok(Err(Error::NoEffect)),
|
||||
}
|
||||
}
|
||||
Ok(within(&nurb, &lowered, tol)?.map(|(c, bound)| (c, Some(bound))))
|
||||
})?
|
||||
}
|
||||
|
||||
/// The record of `changed` and the kernel's certified bound of its distance from `original`,
|
||||
/// when that is within `tol`, else [`Error::PastTolerance`] with the bound.
|
||||
fn within(
|
||||
original: &NurbCurve,
|
||||
changed: &NurbCurve,
|
||||
tol: f64,
|
||||
) -> solidcore::Result<Result<(Curve, f64)>> {
|
||||
let bound = certified_distance(original, changed)?;
|
||||
Ok(if bound <= tol {
|
||||
record_of(changed).map(|c| (c, bound))
|
||||
} else {
|
||||
Err(Error::PastTolerance { bound })
|
||||
})
|
||||
}
|
||||
|
||||
/// The kernel's curve of a record spline or rational spline that is valid and clamped (its
|
||||
/// first and last `degree + 1` knots equal), as the kernel's knot routines take it.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for another curve, one the record refuses, or knots not clamped.
|
||||
pub(crate) fn nurb_of(curve: &Curve) -> Result<NurbCurve> {
|
||||
check_curve(curve, &mut Budget::input()).map_err(|_| Error::Argument)?;
|
||||
let (knots, pw): (&[f64], Vec<CPoint>) = match curve {
|
||||
Curve::Spline { knots, points } => (
|
||||
knots,
|
||||
points
|
||||
.iter()
|
||||
.map(|&[x, y, z]| CPoint::new(x, y, z, NOW))
|
||||
.collect(),
|
||||
),
|
||||
Curve::RationalSpline { knots, homogeneous } => (
|
||||
knots,
|
||||
homogeneous
|
||||
.iter()
|
||||
.map(|&[x, y, z, w]| CPoint::new(x, y, z, w))
|
||||
.collect(),
|
||||
),
|
||||
_ => return Err(Error::Argument),
|
||||
};
|
||||
let degree = knots
|
||||
.len()
|
||||
.checked_sub(pw.len() + 1)
|
||||
.ok_or(Error::Argument)?;
|
||||
let flat =
|
||||
|ends: Option<&[f64]>| ends.is_some_and(|e| e.windows(2).all(|w| w.first() == w.get(1)));
|
||||
let last = knots.len().checked_sub(degree + 1);
|
||||
if !flat(knots.get(..=degree)) || !flat(last.and_then(|s| knots.get(s..))) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
NurbCurve::new(pw, degree, knots.to_vec()).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
/// The record of the kernel's curve `nurb`: a spline where no control point carries a weight,
|
||||
/// a rational spline where every one does.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a curve mixing the two, or one the record refuses (more control
|
||||
/// points or a higher degree than a record holds).
|
||||
pub(crate) fn record_of(nurb: &NurbCurve) -> Result<Curve> {
|
||||
let pw = nurb.cpts();
|
||||
let knots = nurb.knots().to_vec();
|
||||
let curve = if pw.iter().all(|p| p.w == NOW) {
|
||||
Curve::Spline {
|
||||
knots,
|
||||
points: pw.iter().map(|p| [p.x, p.y, p.z]).collect(),
|
||||
}
|
||||
} else if pw.iter().all(|p| p.w != NOW) {
|
||||
Curve::RationalSpline {
|
||||
knots,
|
||||
homogeneous: pw.iter().map(|p| [p.x, p.y, p.z, p.w]).collect(),
|
||||
}
|
||||
} else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
check_curve(&curve, &mut Budget::input()).map_err(|_| Error::Argument)?;
|
||||
Ok(curve)
|
||||
}
|
||||
|
||||
/// Whether `t` lies strictly inside the domain of the (valid) spline `curve`.
|
||||
fn interior(curve: &Curve, t: f64) -> bool {
|
||||
matches!(curve.natural(), Natural::Range(a, b) if a < t && t < b)
|
||||
}
|
||||
|
||||
/// How many times `t` appears among `knots`.
|
||||
fn multiplicity(knots: &[f64], t: f64) -> usize {
|
||||
knots.iter().filter(|k| **k == t).count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::curve::cv_curve;
|
||||
use solidcore::nurbs::knots::Side;
|
||||
|
||||
/// Six control points no cubic with fewer knots passes through.
|
||||
const WAVE: [[f64; 3]; 6] = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
[2.0, -1.0, 0.0],
|
||||
[3.0, 3.0, 0.0],
|
||||
[4.0, 0.0, 1.0],
|
||||
[5.0, 1.0, 0.0],
|
||||
];
|
||||
|
||||
/// The point of `c` at `t`, by the kernel's evaluator.
|
||||
fn at(c: &Curve, t: f64) -> [f64; 3] {
|
||||
let p = nurb_of(c).unwrap().eval(t, Side::Left).unwrap();
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
/// `a` and `b` are the same curve: the same point at 101 parameters across `[0, 1]`.
|
||||
fn same_shape(a: &Curve, b: &Curve, tol: f64) {
|
||||
for i in 0..=100 {
|
||||
let t = f64::from(i) / 100.0;
|
||||
let (p, q) = (at(a, t), at(b, t));
|
||||
assert!(
|
||||
p.iter().zip(q).all(|(x, y)| (x - y).abs() <= tol),
|
||||
"at {t}: {p:?} and {q:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A spline's knots and control point count.
|
||||
fn parts(c: &Curve) -> (&[f64], usize) {
|
||||
match c {
|
||||
Curve::Spline { knots, points } => (knots, points.len()),
|
||||
Curve::RationalSpline { knots, homogeneous } => (knots, homogeneous.len()),
|
||||
_ => panic!("a spline: {c:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_inserted_knot_adds_a_point_and_keeps_the_shape() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let more = insert_knot(&c, 0.3).unwrap();
|
||||
let (knots, count) = parts(&more);
|
||||
assert_eq!(count, WAVE.len() + 1);
|
||||
assert_eq!(multiplicity(knots, 0.3), 1);
|
||||
assert_eq!(knots.len(), parts(&c).0.len() + 1);
|
||||
same_shape(&c, &more, 1e-12);
|
||||
|
||||
// Up to the degree at one value; one more is refused.
|
||||
let mut c3 = c.clone();
|
||||
for _ in 0..3 {
|
||||
c3 = insert_knot(&c3, 0.5).unwrap();
|
||||
}
|
||||
assert_eq!(multiplicity(parts(&c3).0, 0.5), 3);
|
||||
same_shape(&c, &c3, 1e-12);
|
||||
assert_eq!(insert_knot(&c3, 0.5), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_knot_outside_the_domain_or_on_a_curve_without_knots_is_refused() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
for t in [0.0, 1.0, -0.5, 1.5, f64::NAN, f64::INFINITY] {
|
||||
assert_eq!(insert_knot(&c, t), Err(Error::Argument), "{t}");
|
||||
}
|
||||
let line = Curve::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0; 3],
|
||||
};
|
||||
assert_eq!(insert_knot(&line, 0.5), Err(Error::Argument));
|
||||
assert_eq!(remove_knot(&line, 0.5, 1.0), Err(Error::Argument));
|
||||
assert_eq!(set_degree(&line, 3, 0.0), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_knot_inserted_then_removed_gives_the_curve_back() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let more = insert_knot(&c, 0.3).unwrap();
|
||||
let (back, bound) = remove_knot(&more, 0.3, 1e-9).unwrap();
|
||||
assert!(bound < 1e-12, "{bound}");
|
||||
let (knots, count) = parts(&back);
|
||||
assert_eq!(knots, parts(&c).0);
|
||||
assert_eq!(count, WAVE.len());
|
||||
same_shape(&c, &back, 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_inserted_knot_moves_the_curve_by_no_more_than_rounding() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let more = insert_knot(&c, 0.3).unwrap();
|
||||
let bound = certified_distance(&nurb_of(&c).unwrap(), &nurb_of(&more).unwrap()).unwrap();
|
||||
assert!(bound < 1e-12, "{bound}");
|
||||
for i in 0..=200 {
|
||||
let t = f64::from(i) / 200.0;
|
||||
let (p, q) = (at(&c, t), at(&more, t));
|
||||
let gap2: f64 = p.iter().zip(q).map(|(a, b)| (a - b) * (a - b)).sum();
|
||||
assert!(gap2 <= bound * bound, "at {t}: {gap2} past {bound}²");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_repeated_knot_is_removed_one_copy_at_a_time() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let twice = insert_knot(&insert_knot(&c, 0.5).unwrap(), 0.5).unwrap();
|
||||
assert_eq!(multiplicity(parts(&twice).0, 0.5), 2);
|
||||
let (once, _) = remove_knot(&twice, 0.5, 1e-9).unwrap();
|
||||
assert_eq!(multiplicity(parts(&once).0, 0.5), 1);
|
||||
assert_eq!(parts(&once).1, WAVE.len() + 1);
|
||||
let (none, bound) = remove_knot(&once, 0.5, 1e-9).unwrap();
|
||||
assert_eq!(multiplicity(parts(&none).0, 0.5), 0);
|
||||
assert_eq!(parts(&none).0, parts(&c).0);
|
||||
// measured against the curve it started from, within rounding of it
|
||||
assert!(bound < 1e-9, "{bound}");
|
||||
same_shape(&c, &none, 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_knot_the_shape_needs_is_not_removed() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
// The interior knots of six points' cubic are 1/3 and 2/3.
|
||||
let first = parts(&c).0[4];
|
||||
assert!(first > 0.0 && first < 1.0, "{first}");
|
||||
assert!(
|
||||
matches!(remove_knot(&c, first, 1e-6), Err(Error::PastTolerance { bound }) if bound > 1e-6)
|
||||
);
|
||||
// Not a knot, an end knot, or a bad tolerance.
|
||||
assert_eq!(remove_knot(&c, 0.25, 1.0), Err(Error::Argument));
|
||||
assert_eq!(remove_knot(&c, 0.0, 1.0), Err(Error::Argument));
|
||||
assert_eq!(remove_knot(&c, first, -1.0), Err(Error::Argument));
|
||||
assert_eq!(remove_knot(&c, first, f64::NAN), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_raised_degree_keeps_the_shape() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
let (quintic, bound) = set_degree(&c, 5, 0.0).unwrap();
|
||||
assert!(bound.is_some_and(|b| b < 1e-12), "{bound:?}");
|
||||
let (knots, count) = parts(&quintic);
|
||||
assert_eq!(knots.len() - count - 1, 5);
|
||||
assert_eq!(knots.first(), Some(&0.0));
|
||||
assert_eq!(knots.last(), Some(&1.0));
|
||||
same_shape(&c, &quintic, 1e-12);
|
||||
assert_eq!(set_degree(&c, 3, 0.0), Ok((c.clone(), Some(0.0))));
|
||||
assert_eq!(set_degree(&c, 0, 0.0), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
set_degree(&c, MAX_SPLINE_DEGREE + 1, 0.0),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(set_degree(&c, 5, f64::NAN), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_degree_is_lowered_only_within_the_certified_tolerance() {
|
||||
let c = cv_curve(&WAVE, 3).unwrap();
|
||||
// a raised curve comes back down to its own degree, within rounding
|
||||
let (quintic, bound) = set_degree(&c, 5, 0.0).unwrap();
|
||||
assert!(bound.is_some_and(|b| b < 1e-12), "{bound:?}");
|
||||
let (back, bound) = set_degree(&quintic, 3, 1e-9).unwrap();
|
||||
assert!(bound.is_some_and(|b| b <= 1e-9), "{bound:?}");
|
||||
let (knots, count) = parts(&back);
|
||||
assert_eq!(knots.len() - count - 1, 3);
|
||||
same_shape(&c, &back, 1e-9);
|
||||
// the wave is no quadratic
|
||||
assert!(
|
||||
matches!(set_degree(&c, 2, 1e-6), Err(Error::PastTolerance { bound }) if bound > 1e-6)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_raised_arc_stays_on_its_circle() {
|
||||
// A quarter circle of radius 10 about the origin, as a rational quadratic.
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let arc = Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[10.0, 0.0, 0.0, 1.0],
|
||||
[10.0 * w, 10.0 * w, 0.0, w],
|
||||
[0.0, 10.0, 0.0, 1.0],
|
||||
],
|
||||
};
|
||||
let (cubic, bound) = set_degree(&arc, 3, 0.0).unwrap();
|
||||
assert_eq!(bound, None);
|
||||
assert!(matches!(cubic, Curve::RationalSpline { .. }));
|
||||
assert_eq!(parts(&cubic).1, 4);
|
||||
same_shape(&arc, &cubic, 1e-12);
|
||||
for i in 0..=20 {
|
||||
let p = at(&cubic, f64::from(i) / 20.0);
|
||||
let r2 = p[0] * p[0] + p[1] * p[1];
|
||||
assert!((r2 - 100.0).abs() < 1e-9, "r² = {r2}");
|
||||
}
|
||||
// a rational curve's removal and lowering wait for a certified rational bound
|
||||
assert_eq!(set_degree(&cubic, 2, 1.0), Err(Error::Argument));
|
||||
let more = insert_knot(&arc, 0.5).unwrap();
|
||||
assert_eq!(remove_knot(&more, 0.5, 1.0), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclamped_knots_are_refused() {
|
||||
let floating = Curve::Spline {
|
||||
knots: (0..10).map(f64::from).collect(),
|
||||
points: WAVE.to_vec(),
|
||||
};
|
||||
assert_eq!(nurb_of(&floating).err(), Some(Error::Argument));
|
||||
assert_eq!(insert_knot(&floating, 4.5), Err(Error::Argument));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The plane a set of free curves lies in: the kernel's certified
|
||||
//! [`solidcore::curve::plane_of::plane_of`] at the model's zone tolerance, for an extrusion
|
||||
//! along a profile's normal or a section's plane. Ops checks the records and converts.
|
||||
|
||||
use solidcore::curve::plane_of::plane_of as kernel_plane_of;
|
||||
use solidcore::geom::tol::{context_zone_tol_3d, SmallTopoType};
|
||||
use solidcore_record::brep::{check_curve, Budget, Curve};
|
||||
|
||||
use crate::session::run;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A plane: a point of it and its unit normal.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct CurvePlane {
|
||||
/// A point of the plane (the curves' first point).
|
||||
pub origin: [f64; 3],
|
||||
/// The plane's unit normal.
|
||||
pub normal: [f64; 3],
|
||||
}
|
||||
|
||||
/// The plane every point of `curves` lies within the model's zone tolerance of, certified by
|
||||
/// the kernel ([`solidcore::curve::plane_of::plane_of`]: the points whose convex hulls hold
|
||||
/// the curves, each distance bounded with its rounding); `None` when the curves are not
|
||||
/// proved planar to it, or span no plane (all within it of one point or one line). A line is
|
||||
/// read as its segment from `start` to `end`; a circle or an ellipse as the whole conic.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for no curves, a curve the record refuses or a traced intersection
|
||||
/// curve; the kernel's status otherwise.
|
||||
pub fn plane_of(curves: &[Curve]) -> Result<Option<CurvePlane>> {
|
||||
if curves.is_empty() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut kernel_curves = Vec::new();
|
||||
for c in curves {
|
||||
if matches!(c, Curve::SurfaceIntersection { .. }) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
check_curve(c, &mut Budget::input()).map_err(|_| Error::Argument)?;
|
||||
kernel_curves.push(solidcore_convert::curve(c).map_err(|_| Error::Argument)?);
|
||||
}
|
||||
run("curve_plane::plane_of", |k| {
|
||||
let tol = context_zone_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
Ok(kernel_plane_of(&kernel_curves, tol)?.map(|p| CurvePlane {
|
||||
origin: [p.origin.x, p.origin.y, p.origin.z],
|
||||
normal: [p.normal.x, p.normal.y, p.normal.z],
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn spline(points: &[[f64; 3]]) -> Curve {
|
||||
Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: points.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_planar_spline_and_line_give_their_plane() {
|
||||
let wave = spline(&[
|
||||
[0.0, 0.0, 5.0],
|
||||
[10.0, 4.0, 5.0],
|
||||
[20.0, -3.0, 5.0],
|
||||
[30.0, 2.0, 5.0],
|
||||
]);
|
||||
let line = Curve::Line {
|
||||
start: [30.0, 2.0, 5.0],
|
||||
end: [0.0, 0.0, 5.0],
|
||||
};
|
||||
let plane = plane_of(&[wave, line]).unwrap().unwrap();
|
||||
assert_eq!(plane.origin, [0.0, 0.0, 5.0]);
|
||||
assert!(plane.normal[2].abs() > 1.0 - 1e-15, "{:?}", plane.normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_twisted_spline_or_collinear_lines_give_none() {
|
||||
let twisted = spline(&[
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
[0.0, 10.0, 1.0],
|
||||
]);
|
||||
assert_eq!(plane_of(&[twisted]), Ok(None));
|
||||
let a = Curve::Line {
|
||||
start: [0.0, 0.0, 0.0],
|
||||
end: [1.0, 1.0, 1.0],
|
||||
};
|
||||
let b = Curve::Line {
|
||||
start: [2.0, 2.0, 2.0],
|
||||
end: [5.0, 5.0, 5.0],
|
||||
};
|
||||
assert_eq!(plane_of(&[a, b]), Ok(None));
|
||||
assert_eq!(plane_of(&[]), Err(Error::Argument));
|
||||
}
|
||||
}
|
||||
+53
@@ -124,6 +124,43 @@ pub enum Error {
|
||||
/// The join's index: after piece `join`.
|
||||
join: usize,
|
||||
},
|
||||
/// The curves bounding a patch (`crate::surface`) that do not meet end to end: the end of
|
||||
/// curve `join` is not proved within the model's zone tolerance of an end of the next one
|
||||
/// (for the last join, `join + 1` the number of curves, of the first), `gap` apart (the
|
||||
/// kernel's distance between the nearest ends).
|
||||
BoundaryGap {
|
||||
/// The join's index: after curve `join`.
|
||||
join: usize,
|
||||
/// How far apart the ends are.
|
||||
gap: f64,
|
||||
},
|
||||
/// A curve network (`crate::network`) whose u-curve `u` and v-curve `v` do not cross
|
||||
/// once: the kernel's intersector found `count` crossings.
|
||||
NoCrossing {
|
||||
/// The u-curve's index.
|
||||
u: usize,
|
||||
/// The v-curve's index.
|
||||
v: usize,
|
||||
/// How many crossings were found.
|
||||
count: usize,
|
||||
},
|
||||
/// A curve network whose u-curve `u` and v-curve `v` are not proved within the model's
|
||||
/// zone tolerance at the network's common crossing parameters: `gap` bounds how far apart
|
||||
/// they are there (the curves do not cross at one parameter of each family).
|
||||
NetworkGap {
|
||||
/// The u-curve's index.
|
||||
u: usize,
|
||||
/// The v-curve's index.
|
||||
v: usize,
|
||||
/// The certified bound of their distance.
|
||||
gap: f64,
|
||||
},
|
||||
/// A change to a curve that would move it farther than the tolerance asked (a knot
|
||||
/// removed, its degree lowered): the kernel's certified bound of how far it would move.
|
||||
PastTolerance {
|
||||
/// The certified bound of the move.
|
||||
bound: f64,
|
||||
},
|
||||
/// An extension that cannot run as far as asked (a weight of the extended surface would
|
||||
/// vanish, a pole, an apex or a full turn is reached, or its length there cannot be
|
||||
/// measured to the kernel's stop): the longest distance proved to work, when there is one.
|
||||
@@ -484,6 +521,22 @@ impl std::fmt::Display for Error {
|
||||
"the sweep path's pieces {join} and {} do not meet",
|
||||
join.saturating_add(1)
|
||||
),
|
||||
Error::BoundaryGap { join, gap } => write!(
|
||||
f,
|
||||
"the boundary curves {join} and {} do not meet: {gap} apart",
|
||||
join.saturating_add(1)
|
||||
),
|
||||
Error::NoCrossing { u, v, count } => write!(
|
||||
f,
|
||||
"the network's u-curve {u} and v-curve {v} cross {count} times, not once"
|
||||
),
|
||||
Error::NetworkGap { u, v, gap } => write!(
|
||||
f,
|
||||
"the network's u-curve {u} and v-curve {v} are up to {gap} apart where they cross"
|
||||
),
|
||||
Error::PastTolerance { bound } => {
|
||||
write!(f, "the change would move the curve by up to {bound}")
|
||||
}
|
||||
Error::TooFar { largest } => write!(f, "too far to extend (largest {largest:?})"),
|
||||
Error::Exception => write!(f, "solidcore raised an internal exception"),
|
||||
Error::ContactUndecided {
|
||||
|
||||
+6
@@ -37,11 +37,15 @@ pub mod check;
|
||||
pub mod contact;
|
||||
pub mod continuity;
|
||||
pub mod curve;
|
||||
pub mod curve_fit;
|
||||
pub mod curve_knots;
|
||||
pub mod curve_plane;
|
||||
pub mod error;
|
||||
pub mod extend;
|
||||
pub mod file;
|
||||
pub mod label;
|
||||
pub mod measure;
|
||||
pub mod network;
|
||||
pub mod offset;
|
||||
pub mod order;
|
||||
pub mod pcurve;
|
||||
@@ -52,6 +56,8 @@ pub mod session;
|
||||
pub mod split;
|
||||
pub mod step_assembly;
|
||||
pub mod stitch;
|
||||
pub mod surface;
|
||||
pub mod surface_cvs;
|
||||
pub mod sweep;
|
||||
pub mod tangent;
|
||||
pub mod untrim;
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
//! Surfaces through curve networks: the Gordon surface of u-curves crossed by v-curves
|
||||
//! ([`gordon`]), as the kernel builds and certifies it (`solidcore::nurbs::gordon`). The curves
|
||||
//! are the record's ([`Curve`]), each over its natural range; ops checks the counts, converts,
|
||||
//! hands the kernel the model's zone tolerance and names its refusals.
|
||||
|
||||
use solidcore::geom::tol::{context_zone_tol_3d, SmallTopoType};
|
||||
use solidcore::nurbs::gordon::{gordon as kernel_gordon, NetworkRefusal, MAX_CURVES, MIN_CURVES};
|
||||
use solidcore_record::brep::{Brep as Record, Curve};
|
||||
|
||||
use crate::session::run;
|
||||
use crate::surface::{record, sheet, splines};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The refusal as ops names it.
|
||||
fn refused(r: NetworkRefusal) -> Error {
|
||||
match r {
|
||||
NetworkRefusal::Rational => Error::Unsupported("a rational curve in a curve network"),
|
||||
NetworkRefusal::Crossings { u, v, count } => Error::NoCrossing { u, v, count },
|
||||
NetworkRefusal::Order => Error::Argument,
|
||||
NetworkRefusal::Gap { u, v, gap } => Error::NetworkGap { u, v, gap },
|
||||
NetworkRefusal::PastTolerance { bound } => Error::PastTolerance { bound },
|
||||
}
|
||||
}
|
||||
|
||||
/// The Gordon surface through the u-curves `us` and the v-curves `vs`, one face, and the
|
||||
/// certified bound within which every input curve lies on it (at its crossing parameter).
|
||||
///
|
||||
/// The network must be compatible: each family given in order, its curves running one way,
|
||||
/// each u-curve crossing every v-curve once, and every v-curve crossing all the u-curves at one
|
||||
/// parameter of theirs (and the other way round), within the model's zone tolerance; the
|
||||
/// first and last curves of each family cross the other family at its curves' ends. The
|
||||
/// surface's u runs along the u-curves, its v along the v-curves; across each family it is of
|
||||
/// degree `min(3, count - 1)`. Each curve is taken over its natural range (a line from its
|
||||
/// start to its end, a spline over its knots' domain).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for fewer than two or more than 64 curves a family, a curve refused (a
|
||||
/// circle, an ellipse, a traced intersection curve, numbers the kernel refuses), or a family
|
||||
/// out of order; [`Error::Unsupported`] for a rational curve; [`Error::NoCrossing`] for a pair
|
||||
/// that does not cross once; [`Error::NetworkGap`] for a pair not proved within the zone
|
||||
/// tolerance at the common parameters; [`Error::PastTolerance`] for a surface not proved within
|
||||
/// it of every curve; the kernel's status.
|
||||
pub fn gordon(us: &[Curve], vs: &[Curve]) -> Result<(Record, f64)> {
|
||||
let family = MIN_CURVES..=MAX_CURVES;
|
||||
if !family.contains(&us.len()) || !family.contains(&vs.len()) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut bound = None;
|
||||
let made = run("network::gordon", |k| {
|
||||
let us = match splines(k, us)? {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let vs = match splines(k, vs)? {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let tol = context_zone_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
match kernel_gordon(k, &us, &vs, tol)? {
|
||||
Ok(g) => {
|
||||
bound = Some(g.bound);
|
||||
sheet(k, g.surface)
|
||||
}
|
||||
Err(r) => Ok(Err(refused(r))),
|
||||
}
|
||||
});
|
||||
let sheet = record(made)?;
|
||||
Ok((sheet, bound.ok_or(Error::Exception)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// The tests check against closed forms; ops itself computes none.
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solidcore::eval::SurfaceEval;
|
||||
|
||||
fn line(start: [f64; 3], end: [f64; 3]) -> Curve {
|
||||
Curve::Line { start, end }
|
||||
}
|
||||
|
||||
const P: [[f64; 3]; 4] = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 5.0],
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn a_network_of_four_lines_is_the_bilinear_patch() {
|
||||
let (sheet, bound) = gordon(
|
||||
&[line(P[0], P[1]), line(P[2], P[3])],
|
||||
&[line(P[0], P[2]), line(P[1], P[3])],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(bound <= 1e-9, "{bound}");
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
let face = &sheet.faces[0];
|
||||
let eval =
|
||||
SurfaceEval::new(&solidcore_convert::surface(face.parameters()).unwrap()).unwrap();
|
||||
let [[u0, u1], [v0, v1]] = face.domain;
|
||||
for (a, b) in [(0.0, 0.0), (0.3, 0.7), (1.0, 0.5), (0.5, 1.0)] {
|
||||
let p = eval
|
||||
.point([u0 + a * (u1 - u0), v0 + b * (v1 - v0)])
|
||||
.unwrap();
|
||||
let want = |i: usize| {
|
||||
(1.0 - b) * ((1.0 - a) * P[0][i] + a * P[1][i])
|
||||
+ b * ((1.0 - a) * P[2][i] + a * P[3][i])
|
||||
};
|
||||
assert!(
|
||||
(p.x - want(0)).abs() < 1e-9
|
||||
&& (p.y - want(1)).abs() < 1e-9
|
||||
&& (p.z - want(2)).abs() < 1e-9,
|
||||
"{p:?} at ({a}, {b})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_networks_are_refused_naming_the_pair() {
|
||||
let flat = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
];
|
||||
let us = [line(flat[0], flat[1]), line(flat[2], flat[3])];
|
||||
// the second v-curve stops short of the second u-curve
|
||||
assert_eq!(
|
||||
gordon(
|
||||
&us,
|
||||
&[line(flat[0], flat[2]), line(flat[1], [10.0, 5.0, 0.0])]
|
||||
),
|
||||
Err(Error::NoCrossing {
|
||||
u: 1,
|
||||
v: 1,
|
||||
count: 0
|
||||
})
|
||||
);
|
||||
// a family out of order
|
||||
assert_eq!(
|
||||
gordon(
|
||||
&[line(flat[2], flat[3]), line(flat[0], flat[1])],
|
||||
&[line(flat[0], flat[2]), line(flat[1], flat[3])]
|
||||
),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
// one curve a family
|
||||
assert_eq!(gordon(&us[..1], &us), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_network_of_cubic_splines_builds() {
|
||||
// two clamped cubic u-curves (x linear in their parameter, z bent), crossed at t = 0,
|
||||
// 1/2 and 1 by straight v-curves: crossings at common parameters on both families
|
||||
let zs = [[0.0, 2.0, -1.0, 0.0], [1.0, -2.0, 3.0, 1.0]];
|
||||
let ys = [0.0, 10.0];
|
||||
let cubic = |y: f64, z: [f64; 4]| Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: (0..4)
|
||||
.map(|i| [10.0 * f64::from(i) / 3.0, y, z[i as usize]])
|
||||
.collect(),
|
||||
};
|
||||
// the cubic's point at t (Bernstein form)
|
||||
let at = |y: f64, z: [f64; 4], t: f64| {
|
||||
let s = 1.0 - t;
|
||||
let b = [s * s * s, 3.0 * s * s * t, 3.0 * s * t * t, t * t * t];
|
||||
[10.0 * t, y, (0..4).map(|i| b[i] * z[i]).sum::<f64>()]
|
||||
};
|
||||
let us: Vec<Curve> = (0..2).map(|k| cubic(ys[k], zs[k])).collect();
|
||||
let vs: Vec<Curve> = [0.0, 0.5, 1.0]
|
||||
.map(|t| line(at(ys[0], zs[0], t), at(ys[1], zs[1], t)))
|
||||
.to_vec();
|
||||
let (sheet, bound) = gordon(&us, &vs).unwrap();
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
assert!(bound <= 1e-6, "{bound}");
|
||||
}
|
||||
}
|
||||
+593
@@ -0,0 +1,593 @@
|
||||
//! Patches bounded by curves: the Coons patch of four curves meeting end to end round one loop
|
||||
//! (three: its degenerate case, one side collapsed to a corner) and the ruled surface between
|
||||
//! two curves, each a new one-face sheet record whose sides are the curves themselves. The
|
||||
//! curves are the record's ([`Curve`]), each over its natural range; the geometry is the
|
||||
//! kernel's (`make_curves_compatible`, `BSplineSurface::create_coons_patch`,
|
||||
//! `create_ruled_surface`), and so is every distance compared. Ops checks the inputs, turns the
|
||||
//! curves to run end to end, sequences the kernel's calls and converts.
|
||||
|
||||
use solidcore::curve::curve::CurveKind;
|
||||
use solidcore::curve::from_parameters::from_parameters;
|
||||
use solidcore::geom::certified::Bounded;
|
||||
use solidcore::geom::tol::{context_zone_tol_3d, SmallTopoType, USE_DEFAULT};
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_curve::{
|
||||
BSplineCurve, BSplineCurveForm, CanonicalData, Ends, KnotType,
|
||||
};
|
||||
use solidcore::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
|
||||
use solidcore::primitive::api_curve_ops::make_curves_compatible;
|
||||
use solidcore::query::api_queries::point_distance;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::brep::p3::new_brep;
|
||||
use solidcore::topology::brep::sheet::face_on;
|
||||
use solidcore::topology::Brep;
|
||||
use solidcore::SolidError;
|
||||
use solidcore_record::brep::{Brep as Record, Curve, Natural};
|
||||
|
||||
use crate::body::{finish, Body, Expect};
|
||||
use crate::error::status;
|
||||
use crate::session::run;
|
||||
use crate::sweep::{coord_ok, CURVE_MAX_POINTS};
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// A call's own refusal, else the value: as the kernel's calls answer inside [`run`].
|
||||
pub(crate) type Out<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A call's own refusal inside a kernel call.
|
||||
macro_rules! refused {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn point(p: [f64; 3]) -> Point3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
/// A kernel refusal of the arguments as [`Error::Argument`], any other error as its status.
|
||||
fn argument_or_status(e: SolidError) -> Error {
|
||||
match e {
|
||||
SolidError::InvalidInput { .. } => Error::Argument,
|
||||
e => status(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `curve` over its natural range as the kernel's B-spline: a line from its start to its end,
|
||||
/// a polyline over all its segments, a spline over its knots' domain. A circle, an ellipse
|
||||
/// (no ends of their own) and a traced intersection curve are refused.
|
||||
fn spline(k: &Kernel, curve: &Curve) -> solidcore::Result<Out<BSplineCurve>> {
|
||||
let count = match curve {
|
||||
Curve::Line { start, end } => {
|
||||
if !coord_ok(start) || !coord_ok(end) {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
return Ok(
|
||||
BSplineCurve::create_line_segment(3, point(*start), point(*end))
|
||||
.map_err(argument_or_status),
|
||||
);
|
||||
}
|
||||
Curve::Polyline { points } | Curve::Spline { points, .. } => points.len(),
|
||||
Curve::RationalSpline { homogeneous, .. } => homogeneous.len(),
|
||||
Curve::Circle { .. } | Curve::Ellipse { .. } | Curve::SurfaceIntersection { .. } => {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
};
|
||||
if count > CURVE_MAX_POINTS {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
let Natural::Range(lo, hi) = curve.natural() else {
|
||||
return Ok(Err(Error::Argument));
|
||||
};
|
||||
let Ok(record) = solidcore_convert::curve(curve) else {
|
||||
return Ok(Err(Error::Argument));
|
||||
};
|
||||
let made = match from_parameters(k, &record, [lo, hi]) {
|
||||
Ok(made) => made,
|
||||
Err(SolidError::InvalidInput { .. }) => return Ok(Err(Error::Argument)),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
Ok(match made.curve.kind() {
|
||||
CurveKind::BSpline(b) => Ok(b.clone()),
|
||||
_ => Err(Error::Argument),
|
||||
})
|
||||
}
|
||||
|
||||
/// Each record curve as the kernel's B-spline ([`spline`]).
|
||||
pub(crate) fn splines(k: &Kernel, curves: &[Curve]) -> solidcore::Result<Out<Vec<BSplineCurve>>> {
|
||||
let mut out = Vec::new();
|
||||
for c in curves {
|
||||
out.push(refused!(spline(k, c)?));
|
||||
}
|
||||
Ok(Ok(out))
|
||||
}
|
||||
|
||||
/// The kernel's distance between `a` and `b`, with the bound on its rounding.
|
||||
fn apart(a: Point3, b: Point3) -> solidcore::Result<Bounded> {
|
||||
let d = point_distance(a, b)?;
|
||||
Ok(Bounded {
|
||||
value: d.value,
|
||||
bound: d.bound,
|
||||
})
|
||||
}
|
||||
|
||||
/// Which end of a curve with `ends` is proved within `tol` of `p` (`false` its start, `true`
|
||||
/// its end; the start where both are), else how far the nearer end is.
|
||||
fn end_at(p: Point3, ends: &Ends, tol: f64) -> solidcore::Result<std::result::Result<bool, f64>> {
|
||||
let (start, end) = (apart(p, ends.start)?, apart(p, ends.end)?);
|
||||
Ok(if start.at_most(tol).is_yes() {
|
||||
Ok(false)
|
||||
} else if end.at_most(tol).is_yes() {
|
||||
Ok(true)
|
||||
} else if end.value < start.value {
|
||||
Err(end.value)
|
||||
} else {
|
||||
Err(start.value)
|
||||
})
|
||||
}
|
||||
|
||||
/// `c` running the other way over the same interval (the kernel's reversal).
|
||||
fn reverse(c: &mut BSplineCurve) -> solidcore::Result<()> {
|
||||
let interval = c.natural_interval();
|
||||
c.reverse_parameterization(&interval).map(|_| ())
|
||||
}
|
||||
|
||||
/// `curves`, in their order, each turned to run from where the one before it ends, round one
|
||||
/// loop: the first runs towards an end of the second, and every end meets the next curve's
|
||||
/// start (the last's the first's) within `tol`, as the kernel's distance proves. A join not
|
||||
/// proved is [`Error::BoundaryGap`].
|
||||
fn looped(curves: &mut [BSplineCurve], tol: f64) -> solidcore::Result<Out<()>> {
|
||||
let (Some(first), Some(second)) = (curves.first(), curves.get(1)) else {
|
||||
return Ok(Err(Error::Argument));
|
||||
};
|
||||
let (a, b) = (first.ends()?, second.ends()?);
|
||||
match (end_at(a.end, &b, tol)?, end_at(a.start, &b, tol)?) {
|
||||
(Ok(_), _) => {}
|
||||
(Err(_), Ok(_)) => {
|
||||
if let Some(first) = curves.first_mut() {
|
||||
reverse(first)?;
|
||||
}
|
||||
}
|
||||
(Err(g), Err(h)) => {
|
||||
let gap = if h < g { h } else { g };
|
||||
return Ok(Err(Error::BoundaryGap { join: 0, gap }));
|
||||
}
|
||||
}
|
||||
let (mut end, start) = match curves.first() {
|
||||
Some(c) => {
|
||||
let e = c.ends()?;
|
||||
(e.end, e.start)
|
||||
}
|
||||
None => return Ok(Err(Error::Argument)),
|
||||
};
|
||||
for (i, c) in curves.iter_mut().enumerate().skip(1) {
|
||||
match end_at(end, &c.ends()?, tol)? {
|
||||
Ok(false) => {}
|
||||
Ok(true) => reverse(c)?,
|
||||
Err(gap) => {
|
||||
return Ok(Err(Error::BoundaryGap {
|
||||
join: i.saturating_sub(1),
|
||||
gap,
|
||||
}));
|
||||
}
|
||||
}
|
||||
end = c.ends()?.end;
|
||||
}
|
||||
let closing = apart(end, start)?;
|
||||
if !closing.at_most(tol).is_yes() {
|
||||
return Ok(Err(Error::BoundaryGap {
|
||||
join: curves.len().saturating_sub(1),
|
||||
gap: closing.value,
|
||||
}));
|
||||
}
|
||||
Ok(Ok(()))
|
||||
}
|
||||
|
||||
/// A sheet of one face on `surface`, over its whole natural box.
|
||||
pub(crate) fn sheet(k: &mut Kernel, surface: BSplineSurface) -> solidcore::Result<Out<Brep>> {
|
||||
let mut b = new_brep(k, USE_DEFAULT, USE_DEFAULT)?;
|
||||
let surface = Surface::from(surface);
|
||||
let bx = surface.natural_uv_domain_with_nurb(k)?;
|
||||
face_on(k, &mut b, surface, &bx)?;
|
||||
b.refresh_cached_topology();
|
||||
finish(k, b, Expect::FACES)
|
||||
}
|
||||
|
||||
/// A new body's record.
|
||||
pub(crate) fn record(made: Result<Out<Brep>>) -> Result<Record> {
|
||||
Body::new(made??).to_record().map(|(r, _)| r)
|
||||
}
|
||||
|
||||
/// The curve that stays at `p`: degree 1 on `[0, 1]`, both control points `p` (a side of a
|
||||
/// patch collapsed to a corner).
|
||||
fn at_point(p: Point3) -> solidcore::Result<BSplineCurve> {
|
||||
let points = [p, p];
|
||||
BSplineCurve::create_canonical(&CanonicalData {
|
||||
dim: 3,
|
||||
degree: 1,
|
||||
control_points: &points,
|
||||
form: BSplineCurveForm::Unspecified,
|
||||
knot_multiplicities: &[2, 2],
|
||||
knots: &[0.0, 1.0],
|
||||
knot_type: KnotType::Unspecified,
|
||||
weights: None,
|
||||
trim: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `a` and `b` made compatible, exactly: one degree (the higher, by degree elevation), one
|
||||
/// parameter range (`[0, 1]` when theirs differ, by a linear change of parameter) and one knot
|
||||
/// vector (by knot insertion), each curve's shape unchanged (the kernel's
|
||||
/// `make_curves_compatible`).
|
||||
fn compatible(a: BSplineCurve, b: BSplineCurve) -> solidcore::Result<[BSplineCurve; 2]> {
|
||||
let mut pair = [a, b];
|
||||
make_curves_compatible(&mut pair)?;
|
||||
Ok(pair)
|
||||
}
|
||||
|
||||
/// The patch bounded by `curves`, which meet end to end round one loop in the order given
|
||||
/// (each may run either way: it is turned to run on from the one before).
|
||||
///
|
||||
/// Four curves bound the bilinear Coons patch of its four sides, one face. Opposite sides are
|
||||
/// first made compatible (one degree, parameter range and knot vector each pair, exactly), so
|
||||
/// the patch's sides are the four curves themselves: the patch at the side of one curve is that
|
||||
/// curve, point for point at the same fraction of its range (a rational curve, an arc, stays the
|
||||
/// same curve but not the same parameter: the kernel gives the four common end weights). Three curves bound the same patch
|
||||
/// with its fourth side collapsed to the point where the third curve meets the first (a
|
||||
/// degenerate Coons patch: one side of its box is that corner). Each curve is taken over its
|
||||
/// natural range (a line from its start to its end, a spline over its knots' domain).
|
||||
///
|
||||
/// The ends must meet within the model's zone tolerance: the kernel's tolerance within which
|
||||
/// two points are one point of the model (`context_zone_tol_3d`), compared with the kernel's
|
||||
/// distance between the ends and the bound on its rounding, so a join is taken only where it is
|
||||
/// proved. The patch's corners are the middles of the ends that meet there, within that
|
||||
/// tolerance of each curve's end.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for fewer than three curves, or a curve refused (a circle, an ellipse, a
|
||||
/// traced intersection curve, numbers the kernel refuses); [`Error::Unsupported`] for five or
|
||||
/// more curves (no n-sided patch yet); [`Error::BoundaryGap`] for curves that do not meet; the
|
||||
/// kernel's status.
|
||||
pub fn boundary(curves: &[Curve]) -> Result<Record> {
|
||||
if curves.len() < 3 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if curves.len() > 4 {
|
||||
return Err(Error::Unsupported("a patch bounded by five or more curves"));
|
||||
}
|
||||
record(run("surface::boundary", |k| {
|
||||
let mut sides = refused!(splines(k, curves)?);
|
||||
let tol = context_zone_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
refused!(looped(&mut sides, tol)?);
|
||||
// c0 runs P0 to P1, c1 P1 to P2, c2 P2 to P3 and c3 P3 to P0 (three curves: c2 runs P2
|
||||
// to P0, and the fourth side stays at P0). The kernel's sides, in its order: P0 to P1 and
|
||||
// P3 to P2 (running in u, at v min and max), P0 to P3 and P1 to P2 (running in v, at u
|
||||
// min and max).
|
||||
let mut sides = sides.into_iter();
|
||||
let (Some(c0), Some(c1), Some(mut c2)) = (sides.next(), sides.next(), sides.next()) else {
|
||||
return Ok(Err(Error::Argument));
|
||||
};
|
||||
let mut c3 = match sides.next() {
|
||||
Some(c3) => c3,
|
||||
None => at_point(c0.ends()?.start)?,
|
||||
};
|
||||
reverse(&mut c2)?;
|
||||
reverse(&mut c3)?;
|
||||
let [u0, u1] = compatible(c0, c2)?;
|
||||
let [v0, v1] = compatible(c3, c1)?;
|
||||
let mut quad = [u0, u1, v0, v1];
|
||||
match BSplineSurface::create_coons_patch(tol, true, &mut quad, &mut []) {
|
||||
Ok(s) => sheet(k, s),
|
||||
Err(e) => Ok(Err(status(e))),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// The ruled surface between `a` and `b`, one face: `S(s, t) = (1 - t) a(s) + t b(s)`, `s` the
|
||||
/// same fraction of each curve's natural range (the two first made compatible, exactly, as in
|
||||
/// [`boundary`]), `t` from 0 on `a` to 1 on `b`. So the surface's sides are the two curves
|
||||
/// themselves. With `reversed`, `b` runs the other way first (its end is joined to `a`'s start).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a curve refused (as [`boundary`]); the kernel's status.
|
||||
pub fn ruled(a: &Curve, b: &Curve, reversed: bool) -> Result<Record> {
|
||||
record(run("surface::ruled", |k| {
|
||||
let a = refused!(spline(k, a)?);
|
||||
let mut b = refused!(spline(k, b)?);
|
||||
if reversed {
|
||||
reverse(&mut b)?;
|
||||
}
|
||||
let [a, b] = compatible(a, b)?;
|
||||
match BSplineSurface::create_ruled_surface(&a, &b, SurfParamType::V) {
|
||||
Ok(s) => sheet(k, s),
|
||||
Err(e) => Ok(Err(status(e))),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// The tests check against a sphere's radius; ops itself computes none.
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solidcore::eval::SurfaceEval;
|
||||
use solidcore_record::brep::{Face, Frame};
|
||||
|
||||
fn line(start: [f64; 3], end: [f64; 3]) -> Curve {
|
||||
Curve::Line { start, end }
|
||||
}
|
||||
|
||||
/// The point of `face` at the fractions `s`, `t` of its domain.
|
||||
fn at(face: &Face, s: f64, t: f64) -> [f64; 3] {
|
||||
let surface = face.nurbs.as_ref().unwrap_or(&face.surface);
|
||||
let eval = SurfaceEval::new(&solidcore_convert::surface(surface).unwrap()).unwrap();
|
||||
let [[u0, u1], [v0, v1]] = face.domain;
|
||||
let p = eval
|
||||
.point([u0 + s * (u1 - u0), v0 + t * (v1 - v0)])
|
||||
.unwrap();
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3], tol: f64) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() < tol)
|
||||
}
|
||||
|
||||
fn lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
|
||||
[
|
||||
a[0] + t * (b[0] - a[0]),
|
||||
a[1] + t * (b[1] - a[1]),
|
||||
a[2] + t * (b[2] - a[2]),
|
||||
]
|
||||
}
|
||||
|
||||
/// A twisted quadrilateral's corners.
|
||||
const P: [[f64; 3]; 4] = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[10.0, 10.0, 5.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn four_lines_bound_the_bilinear_patch() {
|
||||
// The second line runs backwards: it is turned to run on from the first.
|
||||
let curves = [
|
||||
line(P[0], P[1]),
|
||||
line(P[2], P[1]),
|
||||
line(P[2], P[3]),
|
||||
line(P[3], P[0]),
|
||||
];
|
||||
let sheet = boundary(&curves).unwrap();
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
let face = &sheet.faces[0];
|
||||
let corners = [
|
||||
at(face, 0.0, 0.0),
|
||||
at(face, 1.0, 0.0),
|
||||
at(face, 0.0, 1.0),
|
||||
at(face, 1.0, 1.0),
|
||||
];
|
||||
// its corners are the quadrilateral's
|
||||
for p in P {
|
||||
assert!(
|
||||
corners.iter().any(|c| near(*c, p, 1e-9)),
|
||||
"{p:?} in {corners:?}"
|
||||
);
|
||||
}
|
||||
// the middle of each line is on it, at the middle of a side
|
||||
let sides = [
|
||||
at(face, 0.5, 0.0),
|
||||
at(face, 0.5, 1.0),
|
||||
at(face, 0.0, 0.5),
|
||||
at(face, 1.0, 0.5),
|
||||
];
|
||||
for i in 0..4 {
|
||||
let m = lerp(P[i], P[(i + 1) % 4], 0.5);
|
||||
assert!(
|
||||
sides.iter().any(|c| near(*c, m, 1e-9)),
|
||||
"{m:?} in {sides:?}"
|
||||
);
|
||||
}
|
||||
// and every point is the bilinear blend of its corners
|
||||
for s in [0.1, 0.25, 0.5, 0.8] {
|
||||
for t in [0.2, 0.5, 0.7] {
|
||||
let blend = lerp(
|
||||
lerp(corners[0], corners[1], s),
|
||||
lerp(corners[2], corners[3], s),
|
||||
t,
|
||||
);
|
||||
assert!(near(at(face, s, t), blend, 1e-9), "at ({s}, {t})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The quadratic spline from `a` to `b` whose middle control point stands `up` above the
|
||||
/// chord's middle.
|
||||
fn bulge(a: [f64; 3], b: [f64; 3], up: f64) -> Curve {
|
||||
let m = lerp(a, b, 0.5);
|
||||
Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
points: vec![a, [m[0], m[1], m[2] + up], b],
|
||||
}
|
||||
}
|
||||
|
||||
/// The point of a quadratic Bézier spline at `s`.
|
||||
fn on(c: &Curve, s: f64) -> [f64; 3] {
|
||||
let Curve::Spline { points, .. } = c else {
|
||||
panic!("a spline")
|
||||
};
|
||||
lerp(
|
||||
lerp(points[0], points[1], s),
|
||||
lerp(points[1], points[2], s),
|
||||
s,
|
||||
)
|
||||
}
|
||||
|
||||
/// The four sides of `face`, each at the fraction `s` of its range.
|
||||
fn sides_at(face: &Face, s: f64) -> [[f64; 3]; 4] {
|
||||
[
|
||||
at(face, s, 0.0),
|
||||
at(face, s, 1.0),
|
||||
at(face, 0.0, s),
|
||||
at(face, 1.0, s),
|
||||
]
|
||||
}
|
||||
|
||||
/// Whether the point of `c` at every sampled fraction is a side of `face` at that fraction
|
||||
/// or at its complement (a side may run either way).
|
||||
fn a_side_of(face: &Face, c: &Curve) -> bool {
|
||||
(0..=8).all(|i| {
|
||||
let s = f64::from(i) / 8.0;
|
||||
let p = on(c, s);
|
||||
sides_at(face, s)
|
||||
.iter()
|
||||
.chain(&sides_at(face, 1.0 - s))
|
||||
.any(|q| near(*q, p, 1e-9))
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_splines_are_the_sides_of_their_patch() {
|
||||
let curves: Vec<Curve> = (0..4)
|
||||
.map(|i| bulge(P[i], P[(i + 1) % 4], 1.0 + i as f64))
|
||||
.collect();
|
||||
let sheet = boundary(&curves).unwrap();
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
for c in &curves {
|
||||
assert!(a_side_of(&sheet.faces[0], c), "{c:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_splines_bound_a_patch_with_a_side_at_a_corner() {
|
||||
let p = [[0.0, 0.0, 0.0], [40.0, 0.0, 0.0], [20.0, 35.0, 0.0]];
|
||||
let curves: Vec<Curve> = (0..3).map(|i| bulge(p[i], p[(i + 1) % 3], 1.2)).collect();
|
||||
let sheet = boundary(&curves).unwrap();
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
let face = &sheet.faces[0];
|
||||
for c in &curves {
|
||||
assert!(a_side_of(face, c), "{c:?}");
|
||||
}
|
||||
// the fourth side is the corner where the third curve meets the first
|
||||
let collapsed = [0.0, 0.25, 0.5, 1.0].map(|s| sides_at(face, s));
|
||||
assert!(
|
||||
(0..4).any(|k| collapsed.iter().all(|sides| near(sides[k], p[0], 1e-9))),
|
||||
"{collapsed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_arcs_bound_an_octant_of_a_sphere() {
|
||||
// three quarter circles of radius 10 (rational) meeting round a loop
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let quarter = |a: [f64; 3], corner: [f64; 3], b: [f64; 3]| Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[a[0], a[1], a[2], 1.0],
|
||||
[corner[0] * w, corner[1] * w, corner[2] * w, w],
|
||||
[b[0], b[1], b[2], 1.0],
|
||||
],
|
||||
};
|
||||
let (x, y, z) = ([10.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]);
|
||||
let arcs = [
|
||||
quarter(x, [10.0, 10.0, 0.0], y),
|
||||
quarter(y, [0.0, 10.0, 10.0], z),
|
||||
quarter(z, [10.0, 0.0, 10.0], x),
|
||||
];
|
||||
let sheet = boundary(&arcs).unwrap();
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
// every side's point is on the sphere the arcs lie on
|
||||
for i in 0..=8 {
|
||||
for q in sides_at(&sheet.faces[0], f64::from(i) / 8.0) {
|
||||
let r = q.iter().map(|x| x * x).sum::<f64>().sqrt();
|
||||
assert!((r - 10.0).abs() < 1e-9, "{q:?} at {r}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ruled_surface_has_its_two_curves_as_sides() {
|
||||
let a = bulge([0.0, 0.0, 0.0], [10.0, 0.0, 0.0], 2.0);
|
||||
let (b0, b1) = ([0.0, 5.0, 3.0], [10.0, 5.0, 8.0]);
|
||||
let check = |sheet: &Record, from: [f64; 3], to: [f64; 3]| {
|
||||
assert_eq!(sheet.faces.len(), 1);
|
||||
let face = &sheet.faces[0];
|
||||
for i in 0..=8 {
|
||||
let s = f64::from(i) / 8.0;
|
||||
// a at t = 0, b at t = 1, at the same fraction s
|
||||
assert!(near(at(face, s, 0.0), on(&a, s), 1e-9), "a at {s}");
|
||||
assert!(near(at(face, s, 1.0), lerp(from, to, s), 1e-9), "b at {s}");
|
||||
// straight between them
|
||||
let mid = lerp(on(&a, s), lerp(from, to, s), 0.5);
|
||||
assert!(near(at(face, s, 0.5), mid, 1e-9), "ruling at {s}");
|
||||
}
|
||||
};
|
||||
check(&ruled(&a, &line(b0, b1), false).unwrap(), b0, b1);
|
||||
// reversed: b's end is joined to a's start
|
||||
check(&ruled(&a, &line(b0, b1), true).unwrap(), b1, b0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curves_that_do_not_meet_are_refused_with_the_gap() {
|
||||
let curves = [
|
||||
line(P[0], P[1]),
|
||||
line(P[1], P[2]),
|
||||
line(P[2], [0.0, 10.0, 1.0]),
|
||||
line(P[3], P[0]),
|
||||
];
|
||||
match boundary(&curves) {
|
||||
Err(Error::BoundaryGap { join, gap }) => {
|
||||
assert_eq!(join, 2);
|
||||
assert!((gap - 1.0).abs() < 1e-12, "{gap}");
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
// the last does not come back to the first
|
||||
let open = [
|
||||
line(P[0], P[1]),
|
||||
line(P[1], P[2]),
|
||||
line(P[2], P[3]),
|
||||
line(P[3], [0.0, 2.0, 0.0]),
|
||||
];
|
||||
match boundary(&open) {
|
||||
Err(Error::BoundaryGap { join, gap }) => {
|
||||
assert_eq!(join, 3);
|
||||
assert!((gap - 2.0).abs() < 1e-12, "{gap}");
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_few_or_too_many_curves_and_circles_are_refused() {
|
||||
assert_eq!(
|
||||
boundary(&[line(P[0], P[1]), line(P[1], P[0])]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
let corners = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[12.0, 8.0, 0.0],
|
||||
[5.0, 12.0, 0.0],
|
||||
[-2.0, 8.0, 0.0],
|
||||
];
|
||||
let pentagon: Vec<Curve> = (0..5)
|
||||
.map(|i| line(corners[i], corners[(i + 1) % 5]))
|
||||
.collect();
|
||||
assert!(matches!(boundary(&pentagon), Err(Error::Unsupported(_))));
|
||||
let circle = Curve::Circle {
|
||||
frame: Frame {
|
||||
origin: [0.0; 3],
|
||||
x_axis: [1.0, 0.0, 0.0],
|
||||
y_axis: [0.0, 1.0, 0.0],
|
||||
},
|
||||
radius: 5.0,
|
||||
};
|
||||
assert_eq!(
|
||||
ruled(&circle, &line(P[0], P[1]), false),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
}
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
//! A sheet's control net: read, its control points moved, and the surface rebuilt to new degrees
|
||||
//! and spans with the kernel's certified bound of how far it moved. The sheet is a record of one
|
||||
//! untrimmed face on a non-rational B-spline surface (its face the whole of the surface's
|
||||
//! natural box, as `surface::boundary` and `surface::ruled` make it); the geometry is the
|
||||
//! kernel's. Ops checks the record and converts.
|
||||
|
||||
use solidcore::nurbs::b_spline_surface::BSplineSurface;
|
||||
use solidcore::nurbs::crv_fit::p1::approx_knots;
|
||||
use solidcore::nurbs::geom::Rectangle;
|
||||
use solidcore::nurbs::knots::{clamped_uniform, KnotVector, Side};
|
||||
use solidcore::nurbs::math::{CPoint, NOW};
|
||||
use solidcore::nurbs::srf_basic::{Direction, Surface as NurbSurface};
|
||||
use solidcore::nurbs::srf_deviation::certified_distance;
|
||||
use solidcore::Vec3;
|
||||
use solidcore_record::brep::{
|
||||
knot_domain, Brep, Curve2, Loop, Natural, Surface, MAX_SPLINE_DEGREE,
|
||||
};
|
||||
|
||||
use crate::session::run;
|
||||
use crate::surface::{record, sheet as sheet_of};
|
||||
use crate::sweep::coord_ok;
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The most sample points a rebuild fits (rows times columns).
|
||||
pub const REBUILD_MAX_SAMPLES: usize = 1 << 20;
|
||||
|
||||
/// A sheet's control net: `rows` points along u by `cols` along v, `points[i * cols + j]` the
|
||||
/// point of the i-th u index and the j-th v index.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Net {
|
||||
/// Control points along u.
|
||||
pub rows: usize,
|
||||
/// Control points along v.
|
||||
pub cols: usize,
|
||||
/// The points, row by row (u index major).
|
||||
pub points: Vec<[f64; 3]>,
|
||||
}
|
||||
|
||||
/// The control net of `sheet`'s surface.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a record that is not one face on a non-rational B-spline surface, or
|
||||
/// whose net is not rectangular; [`Error::Unsupported`] for a trimmed face (see [`untrimmed`]).
|
||||
pub fn control_net(sheet: &Brep) -> Result<Net> {
|
||||
let (_, points) = untrimmed(sheet)?;
|
||||
let rows = points.len();
|
||||
let cols = points.first().map_or(0, Vec::len);
|
||||
if cols == 0 || points.iter().any(|r| r.len() != cols) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Net {
|
||||
rows,
|
||||
cols,
|
||||
points: points.iter().flatten().copied().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// `sheet` with the control points `moves` names moved: `((i, j), to)` puts the point of the
|
||||
/// i-th u index and j-th v index at `to` (a later move of one point wins). The knots and
|
||||
/// degrees are kept; the result is a new one-face sheet on the moved surface, its whole box.
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`control_net`]; [`Error::Argument`] for an index past the net or a point not finite or
|
||||
/// past the coordinate bound; the kernel's status otherwise.
|
||||
pub fn move_cvs(sheet: &Brep, moves: &[((usize, usize), [f64; 3])]) -> Result<Brep> {
|
||||
let ((u, v), points) = untrimmed(sheet)?;
|
||||
let mut net = points.to_vec();
|
||||
for &((i, j), to) in moves {
|
||||
if !coord_ok(&to) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
*net.get_mut(i)
|
||||
.and_then(|row| row.get_mut(j))
|
||||
.ok_or(Error::Argument)? = to;
|
||||
}
|
||||
let surface = kernel_surface(u, v, &net)?;
|
||||
record(run("surface_cvs::move_cvs", |k| {
|
||||
sheet_of(k, BSplineSurface::from_gw_nurb(&surface))
|
||||
}))
|
||||
}
|
||||
|
||||
/// `sheet`'s surface refitted to degrees `(deg_u, deg_v)` with `(spans_u, spans_v)` uniform
|
||||
/// spans on `[0, 1]²`, and the kernel's certified bound of how far the refit is from the
|
||||
/// surface at every parameter ([`certified_distance`]), kept when within `tol`. The surface,
|
||||
/// its box mapped onto `[0, 1]²`, is sampled at even steps of each parameter, four per
|
||||
/// control point each way (at most [`REBUILD_MAX_SAMPLES`] in all), and fitted by least
|
||||
/// squares through its boundary rows: each column of samples along u at those parameters,
|
||||
/// then each row of the result along v ([`approx_knots`], The NURBS Book §9.4.3).
|
||||
///
|
||||
/// # Errors
|
||||
/// As [`control_net`]; [`Error::Argument`] for a surface on knots not clamped (a periodic
|
||||
/// one), a degree of 0 or past the record's most, no
|
||||
/// spans, fewer than three or more than a record's control points either way, more samples
|
||||
/// than [`REBUILD_MAX_SAMPLES`], or a `tol` not finite or negative; [`Error::PastTolerance`]
|
||||
/// with the bound when it is past `tol`; the kernel's status otherwise.
|
||||
pub fn rebuild(
|
||||
sheet: &Brep,
|
||||
deg_u: usize,
|
||||
deg_v: usize,
|
||||
spans_u: usize,
|
||||
spans_v: usize,
|
||||
tol: f64,
|
||||
) -> Result<(Brep, f64)> {
|
||||
let ((u, v), points) = untrimmed(sheet)?;
|
||||
let degrees_ok = |d: usize| (1..=MAX_SPLINE_DEGREE).contains(&d);
|
||||
if !(degrees_ok(deg_u) && degrees_ok(deg_v) && tol.is_finite() && tol >= 0.0) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let count = |spans: usize, degree: usize| {
|
||||
spans
|
||||
.checked_add(degree)
|
||||
.filter(|c| (3..=4096).contains(c))
|
||||
.ok_or(Error::Argument)
|
||||
};
|
||||
let (nu, nv) = (count(spans_u, deg_u)?, count(spans_v, deg_v)?);
|
||||
let samples = |c: usize| c.checked_mul(4).and_then(|s| s.checked_add(1));
|
||||
let (su, sv) = (
|
||||
samples(nu).ok_or(Error::Argument)?,
|
||||
samples(nv).ok_or(Error::Argument)?,
|
||||
);
|
||||
if su.checked_mul(sv).is_none_or(|n| n > REBUILD_MAX_SAMPLES) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
// even steps on [0, 1]: the distinct knots of a polyline of that many points
|
||||
let steps = |n: usize| -> Result<Vec<f64>> {
|
||||
let knots = clamped_uniform(n, 1).map_err(|_| Error::Argument)?;
|
||||
Ok(knots.get(1..=n).ok_or(Error::Argument)?.to_vec())
|
||||
};
|
||||
let (pu, pv) = (steps(su)?, steps(sv)?);
|
||||
let uk = clamped_uniform(nu, deg_u).map_err(|_| Error::Argument)?;
|
||||
let vk = clamped_uniform(nv, deg_v).map_err(|_| Error::Argument)?;
|
||||
let original = kernel_surface(u, v, points)?;
|
||||
// A surface on unclamped knots is not sampled: the kernel's surface evaluation keeps
|
||||
// upstream's clamped reading of a domain's ends (N_BasisEval), and the certified bound takes
|
||||
// clamped knots only.
|
||||
if !(clamped(u, original.p()) && clamped(v, original.q())) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let fitted = run("surface_cvs::rebuild", |_| {
|
||||
let mut unit = original.clone();
|
||||
let square = Rectangle {
|
||||
ul: 0.0,
|
||||
ur: 1.0,
|
||||
vb: 0.0,
|
||||
vt: 1.0,
|
||||
};
|
||||
unit.reparam_to_interval(square, Direction::U)?;
|
||||
unit.reparam_to_interval(square, Direction::V)?;
|
||||
// the samples, row a at u = pu[a], column b at v = pv[b]
|
||||
let mut grid: Vec<Vec<Vec3>> = Vec::new();
|
||||
for &a in &pu {
|
||||
grid.push(
|
||||
pv.iter()
|
||||
.map(|&b| unit.eval(a, b, Side::Left, Side::Left))
|
||||
.collect::<solidcore::Result<_>>()?,
|
||||
);
|
||||
}
|
||||
// along u, column by column: rows[i][b] the i-th u point of column b
|
||||
let knots_u = KnotVector::from_slice(&uk)?;
|
||||
let mut along_u: Vec<Vec<Vec3>> = vec![Vec::new(); nu];
|
||||
for b in 0..sv {
|
||||
let column: Vec<Vec3> = grid.iter().filter_map(|row| row.get(b).copied()).collect();
|
||||
let fit = approx_knots(&column, &pu, &knots_u, nu - 1, deg_u)?;
|
||||
for (row, p) in along_u.iter_mut().zip(fit.cpts()) {
|
||||
row.push(Vec3::new(p.x, p.y, p.z));
|
||||
}
|
||||
}
|
||||
// then along v, row by row
|
||||
let knots_v = KnotVector::from_slice(&vk)?;
|
||||
let mut net: Vec<Vec<CPoint>> = Vec::new();
|
||||
for row in &along_u {
|
||||
let fit = approx_knots(row, &pv, &knots_v, nv - 1, deg_v)?;
|
||||
net.push(fit.cpts().to_vec());
|
||||
}
|
||||
let fit = NurbSurface::new(net, deg_u, deg_v, uk.clone(), vk.clone())?;
|
||||
let bound = certified_distance(&unit, &fit)?;
|
||||
Ok(Ok((fit, bound)))
|
||||
})??;
|
||||
let (fit, bound) = fitted;
|
||||
if bound > tol {
|
||||
return Err(Error::PastTolerance { bound });
|
||||
}
|
||||
let made = record(run("surface_cvs::rebuild", |k| {
|
||||
sheet_of(k, BSplineSurface::from_gw_nurb(&fit))
|
||||
}))?;
|
||||
Ok((made, bound))
|
||||
}
|
||||
|
||||
/// Whether the first and last `degree + 1` of `knots` are each one value.
|
||||
fn clamped(knots: &[f64], degree: usize) -> bool {
|
||||
let flat =
|
||||
|ends: Option<&[f64]>| ends.is_some_and(|e| e.windows(2).all(|w| w.first() == w.get(1)));
|
||||
let last = knots.len().checked_sub(degree + 1);
|
||||
flat(knots.get(..=degree)) && flat(last.and_then(|s| knots.get(s..)))
|
||||
}
|
||||
|
||||
/// The kernel's surface of a record net and its knots (degrees from the knot counts).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for knots that do not fit the net.
|
||||
fn kernel_surface(u: &[f64], v: &[f64], net: &[Vec<[f64; 3]>]) -> Result<NurbSurface> {
|
||||
let rows = net.len();
|
||||
let cols = net.first().map_or(0, Vec::len);
|
||||
let p = u.len().checked_sub(rows + 1).ok_or(Error::Argument)?;
|
||||
let q = v.len().checked_sub(cols + 1).ok_or(Error::Argument)?;
|
||||
let pw = net
|
||||
.iter()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|&[x, y, z]| CPoint::new(x, y, z, NOW))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
NurbSurface::new(pw, p, q, u.to_vec(), v.to_vec()).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
/// The knots `(u, v)` and net of the one face of `sheet`, when it is untrimmed: one face on a
|
||||
/// non-rational B-spline surface (no separate NURBS form), its domain the surface's natural
|
||||
/// box, one loop, and every use's pcurve on a side of that box (a spline pcurve whose control
|
||||
/// points all share the side's u or v: by their convex hull, the whole pcurve lies on it).
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for a record of more or fewer faces, another surface or a rational one;
|
||||
/// [`Error::Unsupported`] for a face that is trimmed (or not proved untrimmed: a rational
|
||||
/// pcurve, a pcurve off the box's sides, holes).
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn untrimmed(sheet: &Brep) -> Result<((&[f64], &[f64]), &[Vec<[f64; 3]>])> {
|
||||
let [face] = sheet.faces.as_slice() else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let (
|
||||
Surface::Spline {
|
||||
u_knots,
|
||||
v_knots,
|
||||
points,
|
||||
},
|
||||
None,
|
||||
) = (&face.surface, &face.nurbs)
|
||||
else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let cols = points.first().map_or(0, Vec::len);
|
||||
let (Natural::Range(u0, u1), Natural::Range(v0, v1)) = (
|
||||
knot_domain(u_knots, points.len()),
|
||||
knot_domain(v_knots, cols),
|
||||
) else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
let trimmed = Err(Error::Unsupported("a trimmed face"));
|
||||
if face.domain != [[u0, u1], [v0, v1]] {
|
||||
return trimmed;
|
||||
}
|
||||
let [Loop::Edges { edges }] = face.loops.as_slice() else {
|
||||
return trimmed;
|
||||
};
|
||||
for used in edges {
|
||||
let Curve2::Spline { points: uv, .. } = &used.pcurve else {
|
||||
return trimmed;
|
||||
};
|
||||
let side = |k: usize, at: f64| uv.iter().all(|p| p.get(k) == Some(&at));
|
||||
if !(side(0, u0) || side(0, u1) || side(1, v0) || side(1, v1)) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
Ok(((u_knots, v_knots), points))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::surface::ruled;
|
||||
use solidcore_record::brep::Curve;
|
||||
|
||||
/// The ruled sheet between two cubic splines, a one-face untrimmed sheet.
|
||||
fn sheet() -> Brep {
|
||||
let a = Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: vec![
|
||||
[0.0, 0.0, 0.0],
|
||||
[3.0, 0.0, 2.0],
|
||||
[6.0, 0.0, -1.0],
|
||||
[9.0, 0.0, 0.0],
|
||||
],
|
||||
};
|
||||
let b = Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: vec![
|
||||
[0.0, 8.0, 1.0],
|
||||
[3.0, 8.0, 0.0],
|
||||
[6.0, 8.0, 2.0],
|
||||
[9.0, 8.0, 1.0],
|
||||
],
|
||||
};
|
||||
ruled(&a, &b, false).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_ruled_sheets_net_is_its_two_curves() {
|
||||
let net = control_net(&sheet()).unwrap();
|
||||
assert_eq!(net.rows * net.cols, net.points.len());
|
||||
// one direction runs along the curves (4 points), the other across (2)
|
||||
assert!(
|
||||
(net.rows, net.cols) == (4, 2) || (net.rows, net.cols) == (2, 4),
|
||||
"{net:?}"
|
||||
);
|
||||
for corner in [
|
||||
[0.0, 0.0, 0.0],
|
||||
[9.0, 0.0, 0.0],
|
||||
[0.0, 8.0, 1.0],
|
||||
[9.0, 8.0, 1.0],
|
||||
] {
|
||||
assert!(net.points.contains(&corner), "{corner:?} in {net:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The point of a sheet record's surface at `(u, v)`, by the kernel's evaluator.
|
||||
fn at(sheet: &Brep, u: f64, v: f64) -> [f64; 3] {
|
||||
let ((uk, vk), net) = untrimmed(sheet).unwrap();
|
||||
let p = kernel_surface(uk, vk, net)
|
||||
.unwrap()
|
||||
.eval(u, v, Side::Left, Side::Left)
|
||||
.unwrap();
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
/// A ruled sheet between two cubics of six control points (interior knots 1/3 and 2/3).
|
||||
fn long_sheet() -> Brep {
|
||||
let knots = vec![0.0, 0.0, 0.0, 0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0, 1.0, 1.0, 1.0];
|
||||
let row = |y: f64| Curve::Spline {
|
||||
knots: knots.clone(),
|
||||
points: (0..6)
|
||||
.map(|i| [f64::from(i) * 2.0, y, f64::from(i % 2)])
|
||||
.collect(),
|
||||
};
|
||||
ruled(&row(0.0), &row(5.0), false).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_one_cv_changes_only_its_local_span() {
|
||||
let s = long_sheet();
|
||||
let net = control_net(&s).unwrap();
|
||||
// the direction along the curves has six points; the first of them reaches only the
|
||||
// first third of it
|
||||
let (along_u, first) = (net.rows == 6, (0, 0));
|
||||
let moved = move_cvs(&s, &[(first, [0.0, 0.0, 3.0])]).unwrap();
|
||||
let after = control_net(&moved).unwrap();
|
||||
assert_eq!((after.rows, after.cols), (net.rows, net.cols));
|
||||
assert_eq!(after.points[0], [0.0, 0.0, 3.0]);
|
||||
assert_eq!(&after.points[1..], &net.points[1..]);
|
||||
for i in 0..=20 {
|
||||
for j in 0..=20 {
|
||||
let (a, b) = (f64::from(i) / 20.0, f64::from(j) / 20.0);
|
||||
let along = if along_u { a } else { b };
|
||||
let (p, q) = (at(&s, a, b), at(&moved, a, b));
|
||||
if along >= 1.0 / 3.0 {
|
||||
assert!(
|
||||
p.iter().zip(q).all(|(x, y)| (x - y).abs() < 1e-12),
|
||||
"moved outside its span at ({a}, {b}): {p:?} and {q:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// the moved point's span did move
|
||||
let (a, b) = if along_u { (0.05, 0.0) } else { (0.0, 0.05) };
|
||||
assert_ne!(at(&s, a, b), at(&moved, a, b));
|
||||
// bad moves
|
||||
assert_eq!(
|
||||
move_cvs(&s, &[((net.rows, 0), [0.0; 3])]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
move_cvs(&s, &[((0, 0), [f64::NAN, 0.0, 0.0])]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
|
||||
/// The Coons sheet of four bent cubic Bézier sides: a bicubic Bézier patch.
|
||||
fn bicubic() -> Brep {
|
||||
let side = |points: [[f64; 3]; 4]| Curve::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
points: points.to_vec(),
|
||||
};
|
||||
crate::surface::boundary(&[
|
||||
side([
|
||||
[0.0, 0.0, 0.0],
|
||||
[3.0, 0.0, 2.0],
|
||||
[6.0, 0.0, -1.0],
|
||||
[9.0, 0.0, 0.0],
|
||||
]),
|
||||
side([
|
||||
[9.0, 0.0, 0.0],
|
||||
[9.0, 3.0, 1.0],
|
||||
[9.0, 6.0, -2.0],
|
||||
[9.0, 9.0, 0.0],
|
||||
]),
|
||||
side([
|
||||
[9.0, 9.0, 0.0],
|
||||
[6.0, 9.0, 2.0],
|
||||
[3.0, 9.0, 1.0],
|
||||
[0.0, 9.0, 0.0],
|
||||
]),
|
||||
side([
|
||||
[0.0, 9.0, 0.0],
|
||||
[0.0, 6.0, -1.0],
|
||||
[0.0, 3.0, 2.0],
|
||||
[0.0, 0.0, 0.0],
|
||||
]),
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bicubic_rebuilt_into_its_own_space_is_itself() {
|
||||
let s = bicubic();
|
||||
let net = control_net(&s).unwrap();
|
||||
assert_eq!((net.rows, net.cols), (4, 4), "{net:?}");
|
||||
let (r, bound) = rebuild(&s, 3, 3, 1, 1, 1e-9).unwrap();
|
||||
assert!(bound < 1e-9, "{bound}");
|
||||
for i in 0..=10 {
|
||||
for j in 0..=10 {
|
||||
let (a, b) = (f64::from(i) / 10.0, f64::from(j) / 10.0);
|
||||
let (p, q) = (at(&s, a, b), at(&r, a, b));
|
||||
assert!(p.iter().zip(q).all(|(x, y)| (x - y).abs() <= bound));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rebuild_past_the_tolerance_is_refused_with_its_bound() {
|
||||
let s = bicubic();
|
||||
// the bent patch is no bilinear one
|
||||
assert!(matches!(
|
||||
rebuild(&s, 1, 1, 2, 2, 1e-6),
|
||||
Err(Error::PastTolerance { bound }) if bound > 1e-6
|
||||
));
|
||||
// and a looser tolerance takes the same fit
|
||||
let (_, bound) = rebuild(&s, 2, 2, 2, 2, 100.0).unwrap();
|
||||
assert!(bound > 0.0 && bound <= 100.0);
|
||||
// bad arguments
|
||||
assert_eq!(rebuild(&s, 0, 3, 1, 1, 1.0), Err(Error::Argument));
|
||||
assert_eq!(rebuild(&s, 3, 3, 0, 1, 1.0), Err(Error::Argument));
|
||||
assert_eq!(rebuild(&s, 1, 1, 1, 1, 1.0), Err(Error::Argument));
|
||||
assert_eq!(rebuild(&s, 3, 3, 1, 1, f64::NAN), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_surface_on_unclamped_knots_is_not_sampled() {
|
||||
// the bicubic's u knots made unclamped on the same domain [0, 1]: its face, box and
|
||||
// sides unchanged, so it reads as untrimmed, but its ends are no clamped ends
|
||||
let mut s = bicubic();
|
||||
let Surface::Spline { u_knots, .. } = &mut s.faces[0].surface else {
|
||||
panic!("a spline sheet")
|
||||
};
|
||||
*u_knots = vec![-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0];
|
||||
assert!(control_net(&s).is_ok());
|
||||
assert_eq!(rebuild(&s, 3, 3, 1, 1, 1.0), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_record_of_other_faces_or_a_trimmed_face_is_refused() {
|
||||
let mut two = sheet();
|
||||
let face = two.faces[0].clone();
|
||||
two.faces.push(face);
|
||||
assert_eq!(control_net(&two), Err(Error::Argument));
|
||||
let mut smaller = sheet();
|
||||
smaller.faces[0].domain[0][1] *= 0.5;
|
||||
assert_eq!(
|
||||
control_net(&smaller),
|
||||
Err(Error::Unsupported("a trimmed face"))
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
@@ -71,7 +71,9 @@ use crate::{Error, Result};
|
||||
/// A call's own refusal, else the value: as the kernel's calls answer inside [`run`].
|
||||
type Out<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub mod birail;
|
||||
pub mod railed;
|
||||
mod record;
|
||||
|
||||
/// A coordinate no larger than this (mm, 1000 km), so that the kernel's tolerances stay
|
||||
/// meaningful.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
//! One profile swept along two rails.
|
||||
+329
@@ -0,0 +1,329 @@
|
||||
//! Record curves as sweep curves ([`Curve::from_record`]): each kind of the record's curves over
|
||||
//! an interval of its parameter as the sweep curves that are it, exactly. The geometry is the
|
||||
//! kernel's: a line's ends and a polyline's corners are its points
|
||||
//! (`solidcore::curve::from_parameters`), a circle's arc its definition
|
||||
//! (`solidcore::curve::arc_of`), a spline's or an ellipse's net its B-spline cut to the interval
|
||||
//! (`BSplineCurve::trim`). Ops checks, sequences and converts.
|
||||
|
||||
use solidcore::curve::arc_of::arc_of_circle;
|
||||
use solidcore::curve::curve::{Curve as KCurve, CurveKind};
|
||||
use solidcore::curve::from_parameters::{from_parameters, EdgeCurve};
|
||||
use solidcore::eval::Curve as Eval;
|
||||
use solidcore::geom::vector3d::Point3;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::nurbs::b_spline_curve_n_lib::TrimMode;
|
||||
use solidcore::SolidError;
|
||||
use solidcore_record::brep;
|
||||
|
||||
use super::{argument_or_status, Curve, Out};
|
||||
use crate::session::run;
|
||||
use crate::{Error, Result};
|
||||
|
||||
fn coords(p: Point3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
/// The kernel's curve of `record` over `interval`; a refusal of its numbers is
|
||||
/// [`Error::Argument`].
|
||||
fn made(k: &Kernel, record: &Eval, interval: [f64; 2]) -> solidcore::Result<Out<EdgeCurve>> {
|
||||
match from_parameters(k, record, interval) {
|
||||
Ok(made) => Ok(Ok(made)),
|
||||
Err(SolidError::InvalidInput { .. }) => Ok(Err(Error::Argument)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// The point of `curve` at `t`.
|
||||
fn point(k: &Kernel, curve: &KCurve, t: f64) -> solidcore::Result<[f64; 3]> {
|
||||
curve.evaluate_point(k, t).map(coords)
|
||||
}
|
||||
|
||||
/// `b` cut to `interval` (its own parameter) as a [`Curve::Nurbs`]: its degree, its Euclidean
|
||||
/// control points with their weights (1 for a polynomial curve) and its whole knot vector.
|
||||
fn nurbs(k: &Kernel, b: &BSplineCurve, made: &EdgeCurve) -> solidcore::Result<Out<Curve>> {
|
||||
let mut b = b.clone();
|
||||
let mut interval = made.interval;
|
||||
if let Err(e) = b.trim(k, &mut interval, TrimMode::Silent) {
|
||||
return Ok(Err(argument_or_status(e)));
|
||||
}
|
||||
let net = b.canonical()?;
|
||||
let points: Vec<[f64; 4]> = if net.weights.is_empty() {
|
||||
net.control_points
|
||||
.iter()
|
||||
.map(|p| [p.x, p.y, p.z, 1.0])
|
||||
.collect()
|
||||
} else {
|
||||
net.control_points
|
||||
.iter()
|
||||
.zip(&net.weights)
|
||||
.map(|(p, w)| [p.x, p.y, p.z, *w])
|
||||
.collect()
|
||||
};
|
||||
let knots: Vec<f64> = net
|
||||
.knots
|
||||
.iter()
|
||||
.zip(&net.knot_multiplicities)
|
||||
.flat_map(|(u, m)| std::iter::repeat_n(*u, *m))
|
||||
.collect();
|
||||
Ok(Ok(Curve::Nurbs {
|
||||
degree: net.degree,
|
||||
points,
|
||||
knots,
|
||||
}))
|
||||
}
|
||||
|
||||
impl Curve {
|
||||
/// The record's `curve` over `interval` of its parameter as the sweep curves that are it,
|
||||
/// exactly, in order from the interval's start: a line as one [`Curve::Line`] between its
|
||||
/// points at the interval's ends; a polyline as a line for each of its segments the
|
||||
/// interval reaches (cut where the interval ends inside one; the interval within its range); a circle as one
|
||||
/// [`Curve::Arc`] over the same angles (at most a full turn); an ellipse, a spline or a
|
||||
/// rational spline as one [`Curve::Nurbs`], its B-spline cut to the interval.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`Error::Argument`] for an interval that is not finite and increasing (or, on a polyline,
|
||||
/// past its range), a traced
|
||||
/// intersection curve (no exact sweep curve), a circle's interval past a full turn, or
|
||||
/// numbers the kernel refuses; the kernel's status.
|
||||
pub fn from_record(curve: &brep::Curve, interval: [f64; 2]) -> Result<Vec<Curve>> {
|
||||
let [lo, hi] = interval;
|
||||
if !(lo.is_finite() && hi.is_finite() && lo < hi) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if matches!(curve, brep::Curve::SurfaceIntersection { .. }) {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let Ok(record) = solidcore_convert::curve(curve) else {
|
||||
return Err(Error::Argument);
|
||||
};
|
||||
run("sweep::Curve::from_record", |k| {
|
||||
if let Eval::Circle { frame, radius } = &record {
|
||||
return Ok(match arc_of_circle(frame, *radius, interval) {
|
||||
Ok(arc) => Ok(vec![Curve::Arc {
|
||||
center: coords(arc.centre),
|
||||
axis: coords(arc.axis),
|
||||
reference: coords(arc.reference),
|
||||
radius: arc.radius,
|
||||
start: arc.start,
|
||||
end: arc.end,
|
||||
}]),
|
||||
Err(e) => Err(argument_or_status(e)),
|
||||
});
|
||||
}
|
||||
let made = match made(k, &record, interval)? {
|
||||
Ok(made) => made,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
match (&record, made.curve.kind()) {
|
||||
(Eval::Line { .. }, _) => Ok(Ok(vec![Curve::Line {
|
||||
start: point(k, &made.curve, lo)?,
|
||||
end: point(k, &made.curve, hi)?,
|
||||
}])),
|
||||
(Eval::Polyline { points }, _) => {
|
||||
// the interval within the polyline's range [0, n - 1]: a curve shorter than
|
||||
// asked is no answer
|
||||
let last = points.len().saturating_sub(1) as f64;
|
||||
if lo < 0.0 || hi > last {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
// segment i spans [i, i + 1]: those the interval reaches, cut to it
|
||||
let mut lines = Vec::new();
|
||||
for i in 0..points.len().saturating_sub(1) {
|
||||
let (a, b) = (i as f64, (i + 1) as f64);
|
||||
let (a, b) = (a.max(lo), b.min(hi));
|
||||
if a < b {
|
||||
lines.push(Curve::Line {
|
||||
start: point(k, &made.curve, a)?,
|
||||
end: point(k, &made.curve, b)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(if lines.is_empty() {
|
||||
Err(Error::Argument)
|
||||
} else {
|
||||
Ok(lines)
|
||||
})
|
||||
}
|
||||
(_, CurveKind::BSpline(b)) => Ok(nurbs(k, b, &made)?.map(|c| vec![c])),
|
||||
(_, CurveKind::Ellipse(e)) => Ok(nurbs(k, e.base(), &made)?.map(|c| vec![c])),
|
||||
_ => Ok(Err(Error::Argument)),
|
||||
}
|
||||
})?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// The tests check against closed forms of the record curves (cos, sin, squares); ops itself computes none.
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solidcore_record::brep::{Curve as Record, Frame};
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
|
||||
a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
|
||||
}
|
||||
|
||||
/// The point of a record curve at `t`, by the kernel's evaluator.
|
||||
fn record_at(c: &Record, t: f64) -> [f64; 3] {
|
||||
let e = solidcore::eval::CurveEval::new(&solidcore_convert::curve(c).unwrap()).unwrap();
|
||||
let p = e.point(t).unwrap();
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
/// The point of one sweep curve at the fraction `s` of it, through the record's own
|
||||
/// kinds (a NURBS as a rational spline, an arc by its angle).
|
||||
fn sweep_at(c: &Curve, s: f64) -> [f64; 3] {
|
||||
match c {
|
||||
Curve::Line { start, end } => [0, 1, 2].map(|i| start[i] + s * (end[i] - start[i])),
|
||||
Curve::Arc {
|
||||
center,
|
||||
axis,
|
||||
reference,
|
||||
radius,
|
||||
start,
|
||||
end,
|
||||
} => {
|
||||
let t = start + s * (end - start);
|
||||
let y = [
|
||||
axis[1] * reference[2] - axis[2] * reference[1],
|
||||
axis[2] * reference[0] - axis[0] * reference[2],
|
||||
axis[0] * reference[1] - axis[1] * reference[0],
|
||||
];
|
||||
[0, 1, 2].map(|i| center[i] + radius * (t.cos() * reference[i] + t.sin() * y[i]))
|
||||
}
|
||||
Curve::Nurbs { points, knots, .. } => {
|
||||
let homogeneous = points
|
||||
.iter()
|
||||
.map(|p| [p[0] * p[3], p[1] * p[3], p[2] * p[3], p[3]])
|
||||
.collect();
|
||||
let r = Record::RationalSpline {
|
||||
knots: knots.clone(),
|
||||
homogeneous,
|
||||
};
|
||||
let (a, b) = (knots[0], knots[knots.len() - 1]);
|
||||
record_at(&r, a + s * (b - a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pieces' points at fractions of each against the record's at the same parameter
|
||||
/// (each piece spanning `spans[i]` of the record's parameter, linearly for lines and
|
||||
/// arcs; the NURBS keep the record's parameter).
|
||||
fn same(record: &Record, pieces: &[Curve], spans: &[[f64; 2]]) {
|
||||
assert_eq!(pieces.len(), spans.len(), "{pieces:?}");
|
||||
for (c, [a, b]) in pieces.iter().zip(spans) {
|
||||
for i in 0..=8 {
|
||||
let s = f64::from(i) / 8.0;
|
||||
let (p, q) = (sweep_at(c, s), record_at(record, a + s * (b - a)));
|
||||
assert!(near(p, q), "{c:?} at {s}: {p:?} against {q:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_line_is_its_points_at_the_interval_ends() {
|
||||
let r = Record::Line {
|
||||
start: [1.0, 2.0, 3.0],
|
||||
end: [4.0, 6.0, 3.0],
|
||||
};
|
||||
let c = Curve::from_record(&r, [-1.0, 2.0]).unwrap();
|
||||
same(&r, &c, &[[-1.0, 2.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_polyline_is_a_line_a_segment_cut_to_the_interval() {
|
||||
let r = Record::Polyline {
|
||||
points: vec![[0.0; 3], [5.0, 0.0, 0.0], [5.0, 5.0, 0.0], [0.0, 5.0, 2.0]],
|
||||
};
|
||||
let c = Curve::from_record(&r, [0.5, 2.25]).unwrap();
|
||||
same(&r, &c, &[[0.5, 1.0], [1.0, 2.0], [2.0, 2.25]]);
|
||||
// past either end of its range [0, 3]: refused, not cut short
|
||||
assert_eq!(Curve::from_record(&r, [-0.5, 1.0]), Err(Error::Argument));
|
||||
assert_eq!(Curve::from_record(&r, [2.0, 3.5]), Err(Error::Argument));
|
||||
// its whole range
|
||||
let c = Curve::from_record(&r, [0.0, 3.0]).unwrap();
|
||||
same(&r, &c, &[[0.0, 1.0], [1.0, 2.0], [2.0, 3.0]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_circle_becomes_an_arc() {
|
||||
let r = Record::Circle {
|
||||
frame: Frame {
|
||||
origin: [1.0, 2.0, 3.0],
|
||||
x_axis: [0.0, 1.0, 0.0],
|
||||
y_axis: [0.0, 0.0, 1.0],
|
||||
},
|
||||
radius: 4.0,
|
||||
};
|
||||
let c = Curve::from_record(&r, [0.5, 2.5]).unwrap();
|
||||
assert!(matches!(c[..], [Curve::Arc { .. }]), "{c:?}");
|
||||
same(&r, &c, &[[0.5, 2.5]]);
|
||||
// past a full turn: no arc
|
||||
assert_eq!(Curve::from_record(&r, [0.0, 7.0]), Err(Error::Argument));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splines_and_ellipses_are_their_nets_cut_to_the_interval() {
|
||||
let spline = Record::Spline {
|
||||
knots: vec![0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0],
|
||||
points: vec![[0.0; 3], [2.0, 3.0, 0.0], [5.0, -1.0, 1.0], [8.0, 2.0, 0.0]],
|
||||
};
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let quarter = Record::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
|
||||
homogeneous: vec![
|
||||
[10.0, 0.0, 0.0, 1.0],
|
||||
[10.0 * w, 10.0 * w, 0.0, w],
|
||||
[0.0, 10.0, 0.0, 1.0],
|
||||
],
|
||||
};
|
||||
for (r, interval) in [
|
||||
(&spline, [0.0, 1.0]),
|
||||
(&spline, [0.2, 0.7]),
|
||||
(&quarter, [0.1, 0.9]),
|
||||
] {
|
||||
let c = Curve::from_record(r, interval).unwrap();
|
||||
assert!(matches!(c[..], [Curve::Nurbs { .. }]), "{c:?}");
|
||||
// the cut net keeps the record's parameter
|
||||
let Curve::Nurbs { knots, .. } = &c[0] else {
|
||||
unreachable!()
|
||||
};
|
||||
assert!(
|
||||
(knots[0] - interval[0]).abs() < 1e-12
|
||||
&& (knots[knots.len() - 1] - interval[1]).abs() < 1e-12
|
||||
);
|
||||
same(r, &c, &[interval]);
|
||||
}
|
||||
let ellipse = Record::Ellipse {
|
||||
frame: Frame {
|
||||
origin: [0.0; 3],
|
||||
x_axis: [1.0, 0.0, 0.0],
|
||||
y_axis: [0.0, 1.0, 0.0],
|
||||
},
|
||||
radii: [6.0, 3.0],
|
||||
};
|
||||
let c = Curve::from_record(&ellipse, [0.3, 2.0]).unwrap();
|
||||
assert!(matches!(c[..], [Curve::Nurbs { .. }]), "{c:?}");
|
||||
// on the ellipse, from its point at 0.3 to its point at 2.0
|
||||
let on = |p: [f64; 3]| ((p[0] / 6.0).powi(2) + (p[1] / 3.0).powi(2) - 1.0).abs() < 1e-9;
|
||||
for i in 0..=8 {
|
||||
assert!(on(sweep_at(&c[0], f64::from(i) / 8.0)));
|
||||
}
|
||||
assert!(near(sweep_at(&c[0], 0.0), record_at(&ellipse, 0.3)));
|
||||
assert!(near(sweep_at(&c[0], 1.0), record_at(&ellipse, 2.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_intervals_and_traced_curves_are_refused() {
|
||||
let r = Record::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0, 0.0, 0.0],
|
||||
};
|
||||
assert_eq!(Curve::from_record(&r, [1.0, 1.0]), Err(Error::Argument));
|
||||
assert_eq!(
|
||||
Curve::from_record(&r, [0.0, f64::NAN]),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
//! A body a boolean proved to be a solid, unchanged since, is not proved again as the next
|
||||
//! boolean's operand (#14830), counted by the kernel's volume-sign decisions (feature
|
||||
//! `profile`: `mass::enclosure::volume_sign`, one call per decision).
|
||||
//!
|
||||
//! brep-bench's boolean/drilled-plate: a plate cut by nine drills, one after another. Each cut
|
||||
//! decides the sign of its two operands' volumes and of its result's: 9 cuts, 27 decisions.
|
||||
//! The first operand of cuts 2 to 9 is the cut before's result, which that cut's result
|
||||
//! contract proved: those 8 are taken from the proof, so 19 decisions remain (the plate, the 9
|
||||
//! drills, the 9 results). A primitive is not proved at creation, so its proof stays.
|
||||
#![cfg(feature = "profile")]
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use solidcore_ops::boolean::boolean;
|
||||
use solidcore_ops::session::{self, CallProfile};
|
||||
use solidcore_ops::{make_box, make_cylinder, Body, BooleanOp};
|
||||
|
||||
/// The volume-sign decisions in `calls`.
|
||||
fn decisions(calls: &[CallProfile]) -> u64 {
|
||||
calls
|
||||
.iter()
|
||||
.flat_map(|c| &c.profile.summary)
|
||||
.filter(|s| s.name == "mass::enclosure::volume_sign")
|
||||
.map(|s| s.count)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn all_fixes() {
|
||||
session::set_thread_fixes(solidcore::kernel::UpstreamFixes::NAMES.iter().copied()).unwrap();
|
||||
}
|
||||
|
||||
fn plate() -> Body {
|
||||
make_box([0.0, 0.0, 0.0], [100.0, 20.0, 10.0]).unwrap()
|
||||
}
|
||||
|
||||
/// Drill `i` (1 to 9): radius 2 at (10 i, 10), running past both faces.
|
||||
fn drill(i: u32) -> Body {
|
||||
make_cylinder([10.0 * f64::from(i), 10.0, -1.0], 2.0, 12.0).unwrap()
|
||||
}
|
||||
|
||||
fn cut(a: Body, b: Body) -> Body {
|
||||
boolean(a, b, BooleanOp::Subtract).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The drilled plate, each cut's result kept as it is (`fresh`: false) or replaced by a copy,
|
||||
/// which holds no proof (`fresh`: true), with the decisions the cuts made.
|
||||
fn drilled(fresh: bool) -> (Body, u64) {
|
||||
let drills: Vec<Body> = (1..=9).map(drill).collect();
|
||||
let start = plate();
|
||||
let (body, calls) = session::profiled(|| {
|
||||
drills.into_iter().fold(start, |a, d| {
|
||||
let r = cut(a, d);
|
||||
if fresh {
|
||||
r.copy().unwrap()
|
||||
} else {
|
||||
r
|
||||
}
|
||||
})
|
||||
});
|
||||
(body, decisions(&calls))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drilled_plate_proves_each_cut_result_once() {
|
||||
all_fixes();
|
||||
let (kept, reused) = drilled(false);
|
||||
let (copied, every) = drilled(true);
|
||||
assert_eq!(every, 27, "each cut proves both operands and its result");
|
||||
assert_eq!(
|
||||
reused, 19,
|
||||
"the 8 results proved by the cut before are not proved again"
|
||||
);
|
||||
// the same body either way
|
||||
assert_eq!(kept.counts().unwrap(), copied.counts().unwrap());
|
||||
assert_eq!(
|
||||
kept.volume(1e-9).unwrap().to_bits(),
|
||||
copied.volume(1e-9).unwrap().to_bits()
|
||||
);
|
||||
}
|
||||
|
||||
/// The decisions of cutting drill 2 from `first`, the plate less drill 1.
|
||||
fn second_cut(first: Body) -> u64 {
|
||||
let d = drill(2);
|
||||
let (_, calls) = session::profiled(|| cut(first, d));
|
||||
decisions(&calls)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_proved_body_changed_since_is_proved_again() {
|
||||
all_fixes();
|
||||
// unchanged: the result's proof stands, so the operand drill and the result are decided
|
||||
assert_eq!(second_cut(cut(plate(), drill(1))), 2);
|
||||
// transformed (here by the identity: any edit, through the body's lock, drops the proof
|
||||
// whatever it changes): decided again
|
||||
let moved = cut(plate(), drill(1));
|
||||
moved
|
||||
.transform(&[
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(second_cut(moved), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_proof_made_under_other_fixes_is_not_taken() {
|
||||
all_fixes();
|
||||
let first = cut(plate(), drill(1));
|
||||
// the next call runs with one fillet fix fewer (one a box less a cylinder never reaches):
|
||||
// the proof was made with all of them
|
||||
let names = solidcore::kernel::UpstreamFixes::NAMES;
|
||||
let dropped = names
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|n| n.starts_with("fillet_"))
|
||||
.unwrap();
|
||||
session::set_thread_fixes(names.iter().copied().filter(|n| *n != dropped)).unwrap();
|
||||
let decided = second_cut(first);
|
||||
all_fixes();
|
||||
assert_eq!(decided, 3);
|
||||
}
|
||||
+78
-5
@@ -1604,13 +1604,14 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
None => self.face_extent(fi)?,
|
||||
};
|
||||
let extent = extent.ok_or_else(|| unsupported("a face whose boundary does not lie on its surface"))?;
|
||||
Some(crate::kernel::form::surface_within(&label, extent, tol).ok_or_else(|| unsupported("a face of more than one turn"))?)
|
||||
let (f, d) = crate::kernel::form::surface_within(&label, extent, tol).ok_or_else(|| unsupported("a face of more than one turn"))?;
|
||||
Some((f, d, extent))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let (form, box_on_form) = match form {
|
||||
Some((f, d)) => (Some(f), Some(d)),
|
||||
None => (None, None),
|
||||
let (form, box_on_form, extent) = match form {
|
||||
Some((f, d, e)) => (Some(f), Some(d), Some(e)),
|
||||
None => (None, None, None),
|
||||
};
|
||||
let surface = form.clone().unwrap_or_else(|| label.clone());
|
||||
// How far each edge lies off this surface, so its closed form is checked at that.
|
||||
@@ -1629,8 +1630,14 @@ impl<'b, 'a> Builder<'b, 'a> {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let pcs =
|
||||
let mut pcs =
|
||||
pcurve::pcurves_within(&surface, &uses_of(&self.edges, &edge_loops, made.as_ref())?, tol, box_on_form).map_err(|e| e.to_string())?;
|
||||
// A form ends on a quarter line its face's boundary runs past by up to the tolerance: the
|
||||
// pcurves are put on its range, as the face's domain is ([`face_domain`]), and the edges'
|
||||
// tolerances cover the move.
|
||||
if let (Some(f), Some(e)) = (&form, extent) {
|
||||
onto_form(f, &label, e, tol, &mut pcs);
|
||||
}
|
||||
self.raise_edges(&surface, &edge_loops, &pcs)?;
|
||||
// Signed areas of the loops that close in (u, v).
|
||||
let periods = natural_periods(&surface);
|
||||
@@ -1882,6 +1889,72 @@ fn loop_area(l: &[(u32, bool)], pcs: &[Curve2], periodic: [bool; 2]) -> Option<f
|
||||
(area.abs() > 1e-9 * magnitude && magnitude > 0.0).then_some(0.5 * area)
|
||||
}
|
||||
|
||||
/// The angles `tolerance` (mm) makes along the two directions of a face on `label` over the
|
||||
/// height `v`: on the widest circle of the face about the axis (a cylinder or cone), and on a
|
||||
/// sphere's meridian or a torus's tube. `form::surface_within` ends a face's form on a quarter
|
||||
/// line its domain ends within these angles of; zero along a direction that is no angle.
|
||||
fn form_reach(label: &BSurface, v: [f64; 2], tolerance: f64) -> [f64; 2] {
|
||||
let reach = |radius: f64| if tolerance > 0.0 && radius > 0.0 && radius.is_finite() { tolerance / radius } else { 0.0 };
|
||||
match label {
|
||||
BSurface::Cylinder { radius, .. } => [reach(radius.abs()), 0.0],
|
||||
BSurface::Cone { radius_at_origin, half_angle, .. } => {
|
||||
let slope = half_angle.tan();
|
||||
[reach(v.iter().map(|h| (radius_at_origin + h * slope).abs()).fold(0.0, f64::max)), 0.0]
|
||||
}
|
||||
BSurface::Sphere { radius, .. } => [reach(radius.abs()), reach(radius.abs())],
|
||||
BSurface::Torus { major_radius, minor_radius, .. } => [reach(major_radius.abs() + minor_radius.abs()), reach(minor_radius.abs())],
|
||||
_ => [0.0, 0.0],
|
||||
}
|
||||
}
|
||||
|
||||
/// A face's pcurves on the form `form::surface_within` made for it (`form`, over `domain` on
|
||||
/// the label `label`), put on the form's range. Where the domain ends within `tolerance` of a
|
||||
/// quarter's end the form ends there, but the boundary still lies where its edges do, as much as
|
||||
/// `tolerance` past it: a pcurve control point past the form's end by no more than the form
|
||||
/// parameter of that angle (less than the angle: a quarter's parameter changes by at most 1/√2
|
||||
/// per radian, [`form_reach`]) is moved onto the end. A B-spline is a convex combination of its
|
||||
/// control points, so each pcurve then lies on the form, and the face's domain, which ends at
|
||||
/// the form's, holds it; the pcurve moves by no more than its points did, a gap the edges'
|
||||
/// tolerances are measured to cover (`raise_edges`). A point further out is left as it is, for
|
||||
/// the record check to refuse. The number of control point coordinates moved.
|
||||
fn onto_form(form: &BSurface, label: &BSurface, domain: [[f64; 2]; 2], tolerance: f64, pcs: &mut [Vec<Curve2>]) -> usize {
|
||||
let reach = form_reach(label, domain[1], tolerance);
|
||||
let ranges = solidcore_ops::label::surface_natural(form);
|
||||
let mut moved = 0;
|
||||
let mut onto = |p: &mut [f64; 2]| {
|
||||
for (d, reach) in reach.iter().enumerate() {
|
||||
let (Some(Natural::Range(a, b)), Some(x)) = (ranges.get(d), p.get_mut(d)) else { continue };
|
||||
let end = if *x < *a {
|
||||
*a
|
||||
} else if *x > *b {
|
||||
*b
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if (*x - end).abs() <= *reach {
|
||||
*x = end;
|
||||
moved += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
for c in pcs.iter_mut().flatten() {
|
||||
match c {
|
||||
Curve2::Spline { points, .. } => points.iter_mut().for_each(&mut onto),
|
||||
Curve2::RationalSpline { homogeneous, .. } => {
|
||||
for h in homogeneous.iter_mut().filter(|h| h[2] > 0.0) {
|
||||
let mut p = [h[0] / h[2], h[1] / h[2]];
|
||||
let before = p;
|
||||
onto(&mut p);
|
||||
if p != before {
|
||||
(h[0], h[1]) = (p[0] * h[2], p[1] * h[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
moved
|
||||
}
|
||||
|
||||
/// The parameter box around every pcurve's control points, padded a little, within the
|
||||
/// surface's natural domain (at most one turn where periodic).
|
||||
fn face_domain(surface: &BSurface, pcs: &[Vec<Curve2>]) -> R<[[f64; 2]; 2]> {
|
||||
|
||||
+161
-2
@@ -78,11 +78,16 @@ impl W {
|
||||
}
|
||||
/// The file: millimetres, radians, an uncertainty of 1e-7 mm, every item in one
|
||||
/// representation.
|
||||
fn file(mut self, items: &[u64]) -> p21::Exchange {
|
||||
fn file(self, items: &[u64]) -> p21::Exchange {
|
||||
self.file_with_uncertainty(items, 1e-7)
|
||||
}
|
||||
|
||||
/// [`W::file`] with the file's uncertainty `uncertainty` (mm).
|
||||
fn file_with_uncertainty(mut self, items: &[u64], uncertainty: f64) -> p21::Exchange {
|
||||
let l = self.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))");
|
||||
let a = self.add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))");
|
||||
let s = self.add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())");
|
||||
let u = self.add(format!("UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#{l},'distance_accuracy_value','')"));
|
||||
let u = self.add(format!("UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({}),#{l},'distance_accuracy_value','')", real(uncertainty)));
|
||||
let c = self.add(format!(
|
||||
"(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{u}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#{l},#{a},#{s}))REPRESENTATION_CONTEXT('',''))"
|
||||
));
|
||||
@@ -1613,3 +1618,157 @@ fn a_spline_cone_closed_in_v_gets_a_seam_to_its_apex() {
|
||||
);
|
||||
faces_away(&s.brep, 0, |_| [0.0, 0.0, 1.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcurves_past_a_quarter_line_the_form_ends_on_are_put_on_the_form() {
|
||||
// A cylinder face from 2e-5 rad before the frame's x axis to 0.5 rad (its straight edge 0.2 um
|
||||
// off the quarter line, under the 1e-3 mm tolerance): the form starts on the quarter line,
|
||||
// and the edge's pcurve, on the angle the edge has, lies past the form's range.
|
||||
let r = 10.0;
|
||||
let label = BSurface::Cylinder { frame: Frame { origin: [1.0, 2.0, 3.0], x_axis: [1.0, 0.0, 0.0], y_axis: [0.0, 1.0, 0.0] }, radius: r };
|
||||
let (eps, tol) = (2e-5, 1e-3);
|
||||
let domain = [[-eps, 0.5], [0.0, 5.0]];
|
||||
let (f, d) = crate::kernel::form::surface_within(&label, domain, tol).unwrap();
|
||||
assert_eq!(d[0][0], 0.0, "the form starts on the quarter line: {d:?}");
|
||||
let past = crate::kernel::form::param(-eps);
|
||||
assert!(past < 0.0 && past > -eps, "{past}");
|
||||
let line = |u: f64| Curve2::Spline { knots: vec![0.0, 0.0, 1.0, 1.0], points: vec![[u, 0.0], [u, 5.0]] };
|
||||
// An arc along the bottom from the edge's end, as a rational segment (weights 1, 0.9, 1).
|
||||
let w = 0.9;
|
||||
let arc =
|
||||
Curve2::RationalSpline { knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], homogeneous: vec![[past, 0.0, 1.0], [0.1 * w, 0.0, w], [0.3, 0.0, 1.0]] };
|
||||
let far = crate::kernel::form::param(-0.01);
|
||||
let mut pcs = vec![vec![line(past), arc], vec![line(far)]];
|
||||
assert_eq!(onto_form(&f, &label, domain, tol, &mut pcs), 3, "the line's two ends and the arc's start");
|
||||
assert_eq!(pcs[0][0], line(0.0));
|
||||
match &pcs[0][1] {
|
||||
Curve2::RationalSpline { homogeneous, .. } => assert_eq!(homogeneous[0], [0.0, 0.0, 1.0]),
|
||||
c => panic!("{c:?}"),
|
||||
}
|
||||
// A point further out than the tolerance's angle is left for the check to refuse.
|
||||
assert_eq!(pcs[1][0], line(far));
|
||||
// The moved pcurve is on the form within the tolerance of where the edge is.
|
||||
let (on_form, on_label) = (SurfaceEval::new(&f).unwrap(), SurfaceEval::new(&label).unwrap());
|
||||
for v in [0.0, 2.5, 5.0] {
|
||||
let gap = on_form.point([0.0, v]).unwrap().dist(on_label.point([-eps, v]).unwrap());
|
||||
assert!(gap <= tol && (gap - r * eps).abs() < 1e-9, "{v}: {gap}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A kernel making the missing pcurves as SMLib makes some: each a straight line in (u, v)
|
||||
/// between the true parameters of its edge's ends on the face's NURBS form, with no regard for
|
||||
/// the form's range (a cylinder face; the angle unwrapped to the turn the face's domain is on).
|
||||
struct Projected(std::sync::atomic::AtomicUsize);
|
||||
|
||||
impl Trims for Projected {
|
||||
fn trims(&self, face: &TrimFace<'_>) -> std::result::Result<Vec<Vec<Curve2>>, String> {
|
||||
let BSurface::Cylinder { frame, .. } = face.surface else { return Err("not a cylinder".into()) };
|
||||
let (o, x, y) = (Vec3::from(frame.origin), Vec3::from(frame.x_axis), Vec3::from(frame.y_axis));
|
||||
let z = x.cross(y);
|
||||
let centre = 0.5 * (face.domain[0][0] + face.domain[0][1]);
|
||||
let at = |p: Vec3| -> [f64; 2] {
|
||||
let d = p - o;
|
||||
// A turn is four quarters of form parameter.
|
||||
let mut a = d.dot(y).atan2(d.dot(x));
|
||||
while crate::kernel::form::param(a) < centre - 2.0 {
|
||||
a += TAU;
|
||||
}
|
||||
while crate::kernel::form::param(a) > centre + 2.0 {
|
||||
a -= TAU;
|
||||
}
|
||||
[crate::kernel::form::param(a), d.dot(z)]
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for l in &face.loops {
|
||||
let mut pcs = Vec::new();
|
||||
for u in l {
|
||||
if let Some(pc) = &u.pcurve {
|
||||
pcs.push(pc.clone());
|
||||
continue;
|
||||
}
|
||||
self.0.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let e = face.edges.get(u.edge as usize).ok_or("edge")?;
|
||||
let eval = CurveEval::new(e.nurbs.unwrap_or(e.curve)).map_err(|m| m.to_string())?;
|
||||
let [t0, t1] = e.interval;
|
||||
let (a, b) = (eval.point(t0).ok_or("start")?, eval.point(t1).ok_or("end")?);
|
||||
pcs.push(Curve2::Spline { knots: vec![t0, t0, t1, t1], points: vec![at(a), at(b)] });
|
||||
}
|
||||
out.push(pcs);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// A quarter-cylinder wedge (radius 0.2 mm, height 5 mm, 0 to 100 degrees about +z) whose straight
|
||||
/// edge runs from 1.9 um before the x axis to 1.9 um past it, 0.6 um outside the cylinder, in a
|
||||
/// file of uncertainty 2e-3 mm: the face's extent ends within that of the x axis, so its form
|
||||
/// starts there, while the pcurve its kernel projects for the skewed edge starts before it.
|
||||
/// Every pcurve of the record lies in its face's domain, as SMLib's check asks
|
||||
/// (`SmBrep::ValidatePointers` tests 70 and 71, "Edge endpoints within face domain").
|
||||
#[test]
|
||||
fn a_projected_pcurve_past_the_quarter_line_a_face_form_ends_on_stays_in_the_face_domain() {
|
||||
let (r, h, q): (f64, f64, f64) = (0.2, 5.0, 100f64.to_radians());
|
||||
// The edge 1.9 um either side of the x axis at the rim, and 0.6 um outside the cylinder: more
|
||||
// than half the tolerance off any line on the form, so its pcurve is the kernel's.
|
||||
let (eps, rl) = (1.9e-3 / r, r + 0.6e-3);
|
||||
let mut w = W::default();
|
||||
let v = |w: &mut W, p: V| (w.vertex(p), p);
|
||||
let (p0, p1) = ([rl * eps.cos(), -rl * eps.sin(), 0.0], [rl * eps.cos(), rl * eps.sin(), h]);
|
||||
let nq = [-q.sin(), q.cos(), 0.0];
|
||||
// The axis edge: where the planes through the axis' foot, (p0, p1) and the 100 degree side
|
||||
// meet, up to the top.
|
||||
let np = Vec3::from(p0).cross(Vec3::from(p1));
|
||||
let line = np.cross(Vec3::from(nq));
|
||||
let a1 = [line.x * h / line.z, line.y * h / line.z, h];
|
||||
let (qb, qt) = ([r * q.cos(), r * q.sin(), 0.0], [r * q.cos(), r * q.sin(), h]);
|
||||
let (a0, a1, p0, p1, q0, q1) = (v(&mut w, [0.0; 3]), v(&mut w, a1), v(&mut w, p0), v(&mut w, p1), v(&mut w, qb), v(&mut w, qt));
|
||||
let edge = |w: &mut W, a: (u64, V), b: (u64, V)| w.line_edge(a, b, true, len3(sub3(b.1, a.1)));
|
||||
let (axis, e_p, e_q) = (edge(&mut w, a0, a1), edge(&mut w, p0, p1), edge(&mut w, q0, q1));
|
||||
let (r_pb, r_qb, r_pt, r_qt) = (edge(&mut w, a0, p0), edge(&mut w, a0, q0), edge(&mut w, a1, p1), edge(&mut w, a1, q1));
|
||||
let arc = |w: &mut W, a: (u64, V), b: (u64, V), z: f64| {
|
||||
let c = w.circle([0.0, 0.0, z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], r);
|
||||
w.add(format!("EDGE_CURVE('',#{},#{},#{c},.T.)", a.0, b.0))
|
||||
};
|
||||
let (arc_b, arc_t) = (arc(&mut w, p0, q0, 0.0), arc(&mut w, p1, q1, h));
|
||||
let ax = w.axis([0.0; 3], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
|
||||
let cyl = w.add(format!("CYLINDRICAL_SURFACE('',#{ax},{})", real(r)));
|
||||
let face = |w: &mut W, uses: &[(u64, bool)], surface: u64, same_sense: bool| {
|
||||
let oes: Vec<u64> = uses.iter().map(|&(e, f)| w.oe(e, f)).collect();
|
||||
let b = w.bound(&oes, true, true);
|
||||
w.face(&[b], surface, same_sense)
|
||||
};
|
||||
let (bottom, top) = (w.plane([0.0; 3], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]), w.plane([0.0, 0.0, h], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]));
|
||||
let side_q = w.plane([0.0; 3], nq, [q.cos(), q.sin(), 0.0]);
|
||||
let side_p = w.plane([0.0; 3], [np.x, np.y, np.z], p0.1);
|
||||
let faces = [
|
||||
face(&mut w, &[(e_q, true), (arc_t, false), (e_p, false), (arc_b, true)], cyl, true),
|
||||
face(&mut w, &[(r_qb, true), (arc_b, false), (r_pb, false)], bottom, false),
|
||||
face(&mut w, &[(r_pt, true), (arc_t, true), (r_qt, false)], top, true),
|
||||
face(&mut w, &[(axis, true), (r_qt, true), (e_q, false), (r_qb, false)], side_q, true),
|
||||
face(&mut w, &[(r_pb, true), (e_p, true), (r_pt, false), (axis, false)], side_p, true),
|
||||
];
|
||||
let shell = closed_shell(&mut w, &faces);
|
||||
let solid = w.add(format!("MANIFOLD_SOLID_BREP('wedge',#{shell})"));
|
||||
let kernel = Projected(std::sync::atomic::AtomicUsize::new(0));
|
||||
let read = read_solids_with(&w.file_with_uncertainty(&[solid], 2e-3), &kernel);
|
||||
assert!(read.failures.is_empty(), "{:?}", read.failures);
|
||||
assert!(kernel.0.load(std::sync::atomic::Ordering::Relaxed) > 0, "the skewed edge's pcurve is the kernel's");
|
||||
let b = &read.solids[0].brep;
|
||||
assert_eq!(counts(b), [2, 1, 5, 9, 6]);
|
||||
for (i, f) in b.faces.iter().enumerate() {
|
||||
let d = f.domain;
|
||||
for l in &f.loops {
|
||||
let Loop::Edges { edges } = l else { continue };
|
||||
for u in edges {
|
||||
let [t0, t1] = u.pcurve.interval().unwrap();
|
||||
for t in [t0, t1] {
|
||||
let p = solidcore_ops::pcurve::eval(&u.pcurve, t).unwrap();
|
||||
let out = (d[0][0] - p[0]).max(p[0] - d[0][1]).max(d[1][0] - p[1]).max(p[1] - d[1][1]);
|
||||
assert!(out <= 1e-9, "face {i}, edge {}: {p:?} lies {out:e} outside the domain {d:?}", u.edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
solidcore_ops::record_check::validate(b).unwrap();
|
||||
solidcore_ops::record_check::agreement(b).unwrap();
|
||||
}
|
||||
|
||||
+84
@@ -135,4 +135,88 @@ mod tests {
|
||||
let pts = curve_points(&circle, [0.0, 4.0], MeshTolerance::chord(100.0)).unwrap();
|
||||
assert_eq!(pts.len(), 5, "{pts:?}");
|
||||
}
|
||||
|
||||
/// A closed B-spline as solidcore-ops' `curve_fit::closed_cv_curve` makes it: the polygon
|
||||
/// repeating its first `degree` points after its last, on the kernel's unclamped uniform
|
||||
/// knots, its domain `[0, 1]`.
|
||||
fn closed_cv(points: &[[f64; 3]], degree: usize) -> Curve {
|
||||
let mut wrapped = points.to_vec();
|
||||
wrapped.extend_from_slice(&points[..degree]);
|
||||
Curve::Spline {
|
||||
knots: solidcore::nurbs::knots::periodic_uniform(points.len(), degree).unwrap(),
|
||||
points: wrapped,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_curves_points_close_on_themselves_and_sag_within_tolerance_across_the_seam() {
|
||||
let hexagon = [
|
||||
[10.0, 0.0, 0.0],
|
||||
[5.0, 8.0, 1.0],
|
||||
[-5.0, 8.0, 0.0],
|
||||
[-10.0, 0.0, -1.0],
|
||||
[-5.0, -8.0, 0.0],
|
||||
[5.0, -8.0, 2.0],
|
||||
];
|
||||
for degree in [2, 3, 5] {
|
||||
let curve = closed_cv(&hexagon, degree);
|
||||
let value = solidcore_convert::curve(&curve).unwrap();
|
||||
let eval = CurveEval::new(&value).unwrap();
|
||||
let at = |t: f64| -> Result<Vec3> {
|
||||
eval.point(t)
|
||||
.filter(|p| p.is_finite())
|
||||
.ok_or_else(|| TessError::Failed(format!("no point at {t}")))
|
||||
};
|
||||
for tolerance in [MeshTolerance::chord(0.01), MeshTolerance::display(0.05)] {
|
||||
let pts = curve_points(&curve, [0.0, 1.0], tolerance).unwrap();
|
||||
// The ends are one point: the polyline closes on itself.
|
||||
let (first, last) = (pts[0], pts[pts.len() - 1]);
|
||||
assert!(
|
||||
first.iter().zip(last).all(|(a, b)| (a - b).abs() < 1e-12),
|
||||
"degree {degree}: {first:?} and {last:?}"
|
||||
);
|
||||
assert!(pts.len() >= 5, "degree {degree}: {}", pts.len());
|
||||
|
||||
// The edge sampling the mesher uses for a closed edge (four starting
|
||||
// segments, the same rules) gives the same points, and its parameters.
|
||||
let (tol, turn) = tolerance.rules(0.0);
|
||||
let rules = Rules {
|
||||
tol,
|
||||
max_len: f64::INFINITY,
|
||||
turn,
|
||||
};
|
||||
let ends = [at(0.0).unwrap(), at(1.0).unwrap()];
|
||||
let nodes = bisect(&at, [0.0, 1.0], ends, 4, &rules, f64::INFINITY).unwrap();
|
||||
let mesher: Vec<[f64; 3]> = nodes.points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
assert_eq!(mesher, pts, "degree {degree}");
|
||||
assert!(nodes.capped.is_none(), "degree {degree}");
|
||||
|
||||
// Every chord, the first and last at the seam among them, within the
|
||||
// tolerance of its arc at the points the sampling checks.
|
||||
for (w, t) in nodes.points.windows(2).zip(nodes.params.windows(2)) {
|
||||
for f in [0.25, 0.5, 0.75] {
|
||||
let q = at(t[0] + f * (t[1] - t[0])).unwrap();
|
||||
let sag = q.dist_to_segment(w[0], w[1]);
|
||||
assert!(sag <= tol, "degree {degree}: {sag} on [{}, {}]", t[0], t[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Across the wrap, read as a loop: the curve on either side of the seam
|
||||
// stays near the chords that meet there (between the checked points a
|
||||
// chord's arc may sag past them, never by much).
|
||||
let ring: Vec<Vec3> = nodes.points.clone();
|
||||
for i in 0..=100 {
|
||||
let t = f64::from(i) / 1000.0;
|
||||
for u in [t, 1.0 - t] {
|
||||
let q = at(u).unwrap();
|
||||
let near = ring
|
||||
.windows(2)
|
||||
.map(|w| q.dist_to_segment(w[0], w[1]))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
assert!(near <= 1.5 * tol, "degree {degree}: {near} at {u}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+643
@@ -0,0 +1,643 @@
|
||||
//! Interior isoparm lines of a sheet for display: on each face, lines of constant u and of
|
||||
//! constant v evenly spaced inside its parameter box, drawn on the face's display triangles. The
|
||||
//! plan the mesh is evaluated from ([`crate::plan`], [`crate::TessPlan::evaluate`]) gives each
|
||||
//! face's triangles in its (u, v) and their corners' points; a line is cut by the triangles it
|
||||
//! crosses in (u, v), and each piece mapped linearly onto its triangle. So the lines lie on the
|
||||
//! shaded triangles, never behind them where the surface curves away from the chords (a concave
|
||||
//! neck), and stop where the face's triangles do. Display points, not geometry: drawn, never
|
||||
//! measured or modelled with.
|
||||
|
||||
use solidcore_record::Vec3;
|
||||
use solidcore_record::brep::Brep;
|
||||
|
||||
use crate::mesher::comp;
|
||||
use crate::mesher::surface::eval_surface;
|
||||
use crate::mesher::uv;
|
||||
use crate::{MeshTolerance, Result, TessError, plan};
|
||||
|
||||
/// Most isoparms one direction of a face may ask for.
|
||||
pub const MAX_ISOPARMS: usize = 1024;
|
||||
|
||||
/// Most copies, a period apart, of a line on a closed direction that can cross one face.
|
||||
const MAX_COPIES: usize = 8;
|
||||
|
||||
/// The interior isoparms of every face of `sheet`, on its display mesh: on each face,
|
||||
/// `u_count` lines of constant u at `u0 + i (u1 - u0) / (u_count + 1)`, `i` from 1 to
|
||||
/// `u_count`, over its parameter box `[u0, u1] × [v0, v1]` (the record's `domain`), then
|
||||
/// `v_count` lines of constant v likewise. `tolerance` and `size` are the plan's
|
||||
/// ([`crate::plan`]): the lines are drawn on the triangles of the mesh planned with them. Each
|
||||
/// line is cut by the face's triangles in (u, v) and each piece mapped linearly onto its
|
||||
/// triangle, so every point lies on a triangle of the evaluated mesh, and the lines follow the
|
||||
/// surface as closely as the mesh does; one polyline per run of triangles it crosses, joined
|
||||
/// where consecutive triangles share a side or node (a line broken by a hole or a trim gives
|
||||
/// one each side), and carried along a side into a pole the line runs into (a sphere's
|
||||
/// meridian reaches both poles). A face the plan leaves out, or whose nodes
|
||||
/// the evaluator refuses, gets no lines.
|
||||
///
|
||||
/// The polylines are display points, not geometry: points on the display mesh for drawing,
|
||||
/// never a curve to model with or measure (an isoparm as geometry is the kernel's iso curve of
|
||||
/// the surface).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`TessError::Invalid`] for more than [`MAX_ISOPARMS`] lines a direction; those of
|
||||
/// [`crate::plan`].
|
||||
pub fn isoparms(
|
||||
sheet: &Brep,
|
||||
u_count: usize,
|
||||
v_count: usize,
|
||||
tolerance: MeshTolerance,
|
||||
size: f64,
|
||||
) -> Result<Vec<Vec<[f64; 3]>>> {
|
||||
if u_count > MAX_ISOPARMS || v_count > MAX_ISOPARMS {
|
||||
return Err(TessError::Invalid(format!(
|
||||
"isoparms: {u_count} by {v_count} lines, at most {MAX_ISOPARMS} a direction"
|
||||
)));
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
if u_count == 0 && v_count == 0 {
|
||||
return Ok(out);
|
||||
}
|
||||
let (planned, _) = plan(sheet, &tolerance, size)?;
|
||||
// a node within the plan's chord tolerance of a collapsed side is a pole of the display, as
|
||||
// the planner bounds its own pole reach by it
|
||||
let (reach, _) = tolerance.rules(size);
|
||||
for fp in &planned.faces {
|
||||
let Some(face) = sheet.faces.get(fp.face as usize) else {
|
||||
continue;
|
||||
};
|
||||
// the triangles the mesh keeps (on the face's own nodes), and their corners' points
|
||||
let triangles: Vec<[u32; 3]> = fp
|
||||
.triangles
|
||||
.iter()
|
||||
.filter(|t| t.iter().all(|&i| (i as usize) < fp.uv.len()))
|
||||
.copied()
|
||||
.collect();
|
||||
if triangles.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let nodes = planned.nodes(fp);
|
||||
if nodes.failure.is_some() {
|
||||
continue;
|
||||
}
|
||||
// the record's parameters to the surface's the plan's (u, v) are in
|
||||
let Ok((surface, map)) = eval_surface(face.parameters(), face.domain) else {
|
||||
continue;
|
||||
};
|
||||
let period = uv::domain(&surface).period;
|
||||
// the surface's poles, by the planner's classifier
|
||||
let poles = crate::mesher::poles::Face::new(surface, 0.0);
|
||||
let [[u0, u1], [v0, v1]] = face.domain;
|
||||
for (count, constant_u) in [(u_count, true), (v_count, false)] {
|
||||
for i in 1..=count {
|
||||
let f = i as f64 / (count + 1) as f64;
|
||||
// The line's ends on the parameter box, in the surface's parameters.
|
||||
let (a, b) = if constant_u {
|
||||
let u = u0 + f * (u1 - u0);
|
||||
(map.to_eval([u, v0]), map.to_eval([u, v1]))
|
||||
} else {
|
||||
let v = v0 + f * (v1 - v0);
|
||||
(map.to_eval([u0, v]), map.to_eval([u1, v]))
|
||||
};
|
||||
// The surface's parameter the line keeps (the maps only copy, negate or
|
||||
// reparametrise one coordinate at a time, so one is kept exactly).
|
||||
let d = match (a[0] == b[0], a[1] == b[1]) {
|
||||
(true, false) => 0,
|
||||
(false, true) => 1,
|
||||
_ => continue,
|
||||
};
|
||||
let per = period.get(d).copied().flatten();
|
||||
// a node where the surface is collapsed along d: the line's every value of d
|
||||
// is that point there
|
||||
let pole = |i: u32| {
|
||||
fp.uv
|
||||
.get(i as usize)
|
||||
.is_some_and(|q| poles.pole(*q, d, reach))
|
||||
};
|
||||
let mesh = Triangles {
|
||||
uvs: &fp.uv,
|
||||
at: &nodes.positions,
|
||||
corners: &triangles,
|
||||
};
|
||||
for c in copies(&fp.uv, d, comp(&a, d), per) {
|
||||
out.extend(mesh.line(d, c, &pole));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The values of the surface's parameter `d`, `c` and its copies a whole `period` along, that
|
||||
/// lie within the face's extent in `d` (`c` alone on an open direction).
|
||||
fn copies(uvs: &[[f64; 2]], d: usize, c: f64, period: Option<f64>) -> Vec<f64> {
|
||||
let Some(per) = period.filter(|p| p.is_finite() && *p > 0.0) else {
|
||||
return vec![c];
|
||||
};
|
||||
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for q in uvs {
|
||||
lo = lo.min(comp(q, d));
|
||||
hi = hi.max(comp(q, d));
|
||||
}
|
||||
if lo > hi {
|
||||
return Vec::new();
|
||||
}
|
||||
let first = c + per * ((lo - c) / per).ceil();
|
||||
(0..MAX_COPIES)
|
||||
.map(|k| first + per * k as f64)
|
||||
.take_while(|x| *x < hi)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where a line crosses a triangle's side: at one of its nodes, or inside the side between two.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
enum Crossing {
|
||||
/// At node `i`.
|
||||
Node(u32),
|
||||
/// Inside the side between nodes `i < j`.
|
||||
Side(u32, u32),
|
||||
}
|
||||
|
||||
impl Crossing {
|
||||
/// The nodes the crossing is on.
|
||||
fn nodes(self) -> [Option<u32>; 2] {
|
||||
match self {
|
||||
Crossing::Node(i) => [Some(i), None],
|
||||
Crossing::Side(i, j) => [Some(i), Some(j)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One triangle's piece of a line: its ends' parameter along the line, where they cross the
|
||||
/// triangle's sides, and their points.
|
||||
struct Piece {
|
||||
x: [f64; 2],
|
||||
at: [Crossing; 2],
|
||||
points: [Vec3; 2],
|
||||
}
|
||||
|
||||
/// A face's display triangles in (u, v): its nodes' parameters and points, and the triangles.
|
||||
struct Triangles<'a> {
|
||||
uvs: &'a [[f64; 2]],
|
||||
at: &'a [Vec3],
|
||||
/// Each triangle's three nodes.
|
||||
corners: &'a [[u32; 3]],
|
||||
}
|
||||
|
||||
impl Triangles<'_> {
|
||||
/// The line where parameter `d` is `c` on the triangles: each triangle it crosses gives
|
||||
/// the piece between the two sides it crosses, its ends the sides' points at the crossing
|
||||
/// (straight on its flat triangle). A side is crossed by the half-open rule (one end at or
|
||||
/// below `c`, the other above), so a line through a node or along a side between two
|
||||
/// triangles is taken once. Pieces join where one ends on the very side or node of the mesh
|
||||
/// the next starts on (two triangles sharing it), never by distance. A polyline that ends on
|
||||
/// a side at a node where the surface collapses along `d` (`pole`: a pole the line runs into,
|
||||
/// every value of `d` the same point there) is carried along that side to the node, so a
|
||||
/// line into a pole reaches it.
|
||||
fn line(&self, d: usize, c: f64, pole: &dyn Fn(u32) -> bool) -> Vec<Vec<[f64; 3]>> {
|
||||
let o = usize::from(d == 0);
|
||||
let corner = |i: u32| Some((*self.uvs.get(i as usize)?, *self.at.get(i as usize)?));
|
||||
let mut pieces: Vec<Piece> = Vec::new();
|
||||
for &[i0, i1, i2] in self.corners {
|
||||
let (Some(a), Some(b), Some(e)) = (corner(i0), corner(i1), corner(i2)) else {
|
||||
continue;
|
||||
};
|
||||
let mut hits: Vec<(f64, Crossing, Vec3)> = Vec::with_capacity(2);
|
||||
for ((ip, p), (iq, q)) in [((i0, a), (i1, b)), ((i1, b), (i2, e)), ((i2, e), (i0, a))] {
|
||||
let (pd, qd) = (comp(&p.0, d), comp(&q.0, d));
|
||||
if (pd <= c) != (qd <= c) {
|
||||
let s = (c - pd) / (qd - pd);
|
||||
let x = comp(&p.0, o) + s * (comp(&q.0, o) - comp(&p.0, o));
|
||||
// exactly at a node (s is exactly 0 or 1 there): that node, its own point
|
||||
let (at, point) = if s == 0.0 {
|
||||
(Crossing::Node(ip), p.1)
|
||||
} else if s == 1.0 {
|
||||
(Crossing::Node(iq), q.1)
|
||||
} else {
|
||||
(
|
||||
Crossing::Side(ip.min(iq), ip.max(iq)),
|
||||
p.1 + (q.1 - p.1) * s,
|
||||
)
|
||||
};
|
||||
hits.push((x, at, point));
|
||||
}
|
||||
}
|
||||
if let [(x0, a0, p0), (x1, a1, p1)] = hits.as_slice() {
|
||||
if x0 < x1 {
|
||||
pieces.push(Piece {
|
||||
x: [*x0, *x1],
|
||||
at: [*a0, *a1],
|
||||
points: [*p0, *p1],
|
||||
});
|
||||
} else if x1 < x0 {
|
||||
pieces.push(Piece {
|
||||
x: [*x1, *x0],
|
||||
at: [*a1, *a0],
|
||||
points: [*p1, *p0],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pieces.sort_by(|a, b| a.x[0].total_cmp(&b.x[0]));
|
||||
// each piece by where it starts; a piece no other ends where it starts heads a polyline
|
||||
let mut starting: std::collections::HashMap<Crossing, Vec<usize>> =
|
||||
std::collections::HashMap::new();
|
||||
for (k, piece) in pieces.iter().enumerate() {
|
||||
starting.entry(piece.at[0]).or_default().push(k);
|
||||
}
|
||||
let ends: std::collections::HashSet<Crossing> = pieces.iter().map(|p| p.at[1]).collect();
|
||||
let mut used = vec![false; pieces.len()];
|
||||
let xyz = |p: Vec3| [p.x, p.y, p.z];
|
||||
let mut lines = Vec::new();
|
||||
for head in 0..pieces.len() {
|
||||
let Some(first) = pieces.get(head) else {
|
||||
continue;
|
||||
};
|
||||
if used.get(head).copied().unwrap_or(true) || ends.contains(&first.at[0]) {
|
||||
continue;
|
||||
}
|
||||
let mut line = Vec::new();
|
||||
if let Some(p) = self.to_pole(first.at[0], first.x[0], o, false, pole) {
|
||||
line.push(xyz(p));
|
||||
}
|
||||
line.push(xyz(first.points[0]));
|
||||
let mut k = head;
|
||||
while let Some(piece) = pieces.get(k) {
|
||||
if let Some(u) = used.get_mut(k) {
|
||||
*u = true;
|
||||
}
|
||||
line.push(xyz(piece.points[1]));
|
||||
let next = starting
|
||||
.get(&piece.at[1])
|
||||
.and_then(|ks| ks.iter().find(|&&j| !used.get(j).copied().unwrap_or(true)));
|
||||
if let Some(&j) = next {
|
||||
k = j;
|
||||
} else {
|
||||
if let Some(p) = self.to_pole(piece.at[1], piece.x[1], o, true, pole) {
|
||||
line.push(xyz(p));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
line.dedup();
|
||||
if line.len() >= 2 {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
/// The point of a node of `at` where the surface collapses along the line (`pole`) and that
|
||||
/// lies on along the line from `x` (beyond it at the line's `end`, before it at its start),
|
||||
/// if there is one: the pole the line runs into along that side.
|
||||
fn to_pole(
|
||||
&self,
|
||||
at: Crossing,
|
||||
x: f64,
|
||||
o: usize,
|
||||
end: bool,
|
||||
pole: &dyn Fn(u32) -> bool,
|
||||
) -> Option<Vec3> {
|
||||
at.nodes().into_iter().flatten().find_map(|i| {
|
||||
let q = self.uvs.get(i as usize)?;
|
||||
let ahead = if end {
|
||||
comp(q, o) >= x
|
||||
} else {
|
||||
comp(q, o) <= x
|
||||
};
|
||||
if ahead && pole(i) {
|
||||
self.at.get(i as usize).copied()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fixtures::{fixture, size_of};
|
||||
use solidcore_record::brep::Surface;
|
||||
|
||||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
|
||||
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0],
|
||||
]
|
||||
}
|
||||
|
||||
/// How far `p` is from the segment `a`–`b`.
|
||||
fn off_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
let (ab, ap) = (sub(b, a), sub(p, a));
|
||||
let t = (dot(ap, ab) / dot(ab, ab)).clamp(0.0, 1.0);
|
||||
let q = [a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]];
|
||||
dot(sub(p, q), sub(p, q)).sqrt()
|
||||
}
|
||||
|
||||
/// Whether `p` lies on an edge of `b` (all of `b`'s edges straight).
|
||||
fn on_an_edge(b: &Brep, p: [f64; 3]) -> bool {
|
||||
b.edges.iter().any(|e| {
|
||||
let [a, c] = e.vertices.map(|v| b.vertices[v as usize].point);
|
||||
off_segment(p, a, c) < 1e-9
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_boxs_lines_are_at_their_parameters_and_end_on_its_edges() {
|
||||
let (b, size) = (fixture("box_10x6x4"), size_of("box_10x6x4"));
|
||||
let lines = isoparms(&b, 3, 0, MeshTolerance::chord(0.01), size).unwrap();
|
||||
assert!(lines.len() >= 12, "{}", lines.len());
|
||||
for line in &lines {
|
||||
// a plane's lines are straight: every point on the chord between its ends
|
||||
let (p, q) = (line[0], line[line.len() - 1]);
|
||||
assert!(
|
||||
line.iter().all(|x| off_segment(*x, p, q) < 1e-9),
|
||||
"{line:?}"
|
||||
);
|
||||
assert!(on_an_edge(&b, p) && on_an_edge(&b, q), "{line:?}");
|
||||
// on the face whose plane holds it, at one of its three u
|
||||
let at_its_u = b.faces.iter().any(|f| {
|
||||
let Surface::Plane {
|
||||
origin,
|
||||
u_axis,
|
||||
v_axis,
|
||||
} = f.parameters()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let n = cross(*u_axis, *v_axis);
|
||||
let on_plane = |x: [f64; 3]| dot(sub(x, *origin), n).abs() < 1e-9;
|
||||
let u = |x: [f64; 3]| dot(sub(x, *origin), *u_axis) / dot(*u_axis, *u_axis);
|
||||
let [u0, u1] = f.domain[0];
|
||||
on_plane(p)
|
||||
&& on_plane(q)
|
||||
&& (u(p) - u(q)).abs() < 1e-9
|
||||
&& (1..=3).any(|i| (u(p) - (u0 + f64::from(i) * (u1 - u0) / 4.0)).abs() < 1e-9)
|
||||
});
|
||||
assert!(at_its_u, "{line:?}");
|
||||
}
|
||||
// none asked, none drawn; too many refused
|
||||
assert!(
|
||||
isoparms(&b, 0, 0, MeshTolerance::chord(0.01), size)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(matches!(
|
||||
isoparms(&b, MAX_ISOPARMS + 1, 0, MeshTolerance::chord(0.01), size),
|
||||
Err(TessError::Invalid(_))
|
||||
));
|
||||
}
|
||||
|
||||
/// `p`'s distance from the axis of a face's cylinder, and its radius.
|
||||
fn radial(s: &Surface, p: [f64; 3]) -> Option<(f64, f64)> {
|
||||
let Surface::Cylinder { frame, radius } = s else {
|
||||
return None;
|
||||
};
|
||||
let w = sub(p, frame.origin);
|
||||
let (x, y) = (dot(w, frame.x_axis), dot(w, frame.y_axis));
|
||||
Some((x.hypot(y), *radius))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_stop_at_a_hole() {
|
||||
let (b, size) = (fixture("box_with_hole"), size_of("box_with_hole"));
|
||||
let tol = 0.01;
|
||||
let lines = isoparms(&b, 7, 7, MeshTolerance::chord(tol), size).unwrap();
|
||||
let hole = b
|
||||
.faces
|
||||
.iter()
|
||||
.find(|f| matches!(f.surface, Surface::Cylinder { .. }))
|
||||
.unwrap();
|
||||
// ends on the hole's rim: on its sampled circle, within the tolerance inside it
|
||||
let on_rim = |p: [f64; 3]| {
|
||||
let (r, radius) = radial(&hole.surface, p).unwrap();
|
||||
(radius - tol * 1.01..=radius + 1e-9).contains(&r)
|
||||
};
|
||||
let at_hole = lines
|
||||
.iter()
|
||||
.filter(|l| on_rim(l[0]) || on_rim(l[l.len() - 1]))
|
||||
.count();
|
||||
assert!(at_hole > 0, "no line ends at the hole");
|
||||
// and no line crosses it: no chord's middle in the hole on the faces it cuts
|
||||
for l in &lines {
|
||||
for w in l.windows(2) {
|
||||
let m = [
|
||||
(w[0][0] + w[1][0]) / 2.0,
|
||||
(w[0][1] + w[1][1]) / 2.0,
|
||||
(w[0][2] + w[1][2]) / 2.0,
|
||||
];
|
||||
let (r, radius) = radial(&hole.surface, m).unwrap();
|
||||
let on_wall = on_rim(w[0]) && on_rim(w[1]);
|
||||
assert!(
|
||||
on_wall || r >= radius - tol * 1.01,
|
||||
"{w:?} crosses the hole"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cylinders_lines_sag_within_the_tolerance() {
|
||||
let (b, size) = (fixture("cylinder_r10_h5"), size_of("cylinder_r10_h5"));
|
||||
let side = b
|
||||
.faces
|
||||
.iter()
|
||||
.find(|f| matches!(f.surface, Surface::Cylinder { .. }))
|
||||
.unwrap();
|
||||
for tol in [0.1, 0.01] {
|
||||
let lines = isoparms(&b, 4, 4, MeshTolerance::chord(tol), size).unwrap();
|
||||
let mut round = 0;
|
||||
for l in &lines {
|
||||
let on_side = l.iter().all(|p| {
|
||||
let (r, radius) = radial(&side.surface, *p).unwrap();
|
||||
(r - radius).abs() <= tol * 1.0001
|
||||
});
|
||||
if !on_side || l.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
round += 1;
|
||||
for w in l.windows(2) {
|
||||
let m = [
|
||||
(w[0][0] + w[1][0]) / 2.0,
|
||||
(w[0][1] + w[1][1]) / 2.0,
|
||||
(w[0][2] + w[1][2]) / 2.0,
|
||||
];
|
||||
let (r, radius) = radial(&side.surface, m).unwrap();
|
||||
assert!(radius - r <= tol * 1.0001, "sag {} at {tol}", radius - r);
|
||||
}
|
||||
}
|
||||
assert!(round > 0, "no line round the cylinder at {tol}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The centre and radius of `b`'s sphere face.
|
||||
fn sphere(b: &Brep) -> ([f64; 3], f64) {
|
||||
b.faces
|
||||
.iter()
|
||||
.find_map(|f| match &f.surface {
|
||||
Surface::Sphere { frame, radius } => Some((frame.origin, *radius)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The lines of `b` that lie on its sphere (every point at its radius), each checked: its
|
||||
/// chords sag at most `tol` inside the sphere.
|
||||
fn sphere_lines(b: &Brep, lines: &[Vec<[f64; 3]>], tol: f64) -> Vec<Vec<[f64; 3]>> {
|
||||
let (c, r) = sphere(b);
|
||||
let from_centre = |p: [f64; 3]| dot(sub(p, c), sub(p, c)).sqrt();
|
||||
let on: Vec<Vec<[f64; 3]>> = lines
|
||||
.iter()
|
||||
.filter(|l| {
|
||||
l.iter()
|
||||
.all(|p| (from_centre(*p) - r).abs() <= tol * 1.0001)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
for l in &on {
|
||||
for w in l.windows(2) {
|
||||
let m = [0, 1, 2].map(|i| (w[0][i] + w[1][i]) / 2.0);
|
||||
let sag = r - from_centre(m);
|
||||
assert!((-1e-9..=tol * 1.0001).contains(&sag), "sag {sag} on {w:?}");
|
||||
}
|
||||
}
|
||||
on
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_whole_spheres_lines_are_one_run_each() {
|
||||
let (b, size) = (fixture("sphere_r12"), size_of("sphere_r12"));
|
||||
let tol = 0.01;
|
||||
let lines = isoparms(&b, 5, 3, MeshTolerance::chord(tol), size).unwrap();
|
||||
// one face, its boundary a seam from pole to pole: every line is one run
|
||||
assert_eq!(lines.len(), 8, "{}", lines.len());
|
||||
let on = sphere_lines(&b, &lines, tol);
|
||||
assert_eq!(on.len(), 8);
|
||||
// the meridians run from pole to pole, the parallels close
|
||||
let pole = |p: [f64; 3]| p[0].hypot(p[1]) < 1e-6;
|
||||
let meridians = on
|
||||
.iter()
|
||||
.filter(|l| pole(l[0]) && pole(l[l.len() - 1]))
|
||||
.count();
|
||||
let rounds = on
|
||||
.iter()
|
||||
.filter(|l| dot(sub(l[0], l[l.len() - 1]), sub(l[0], l[l.len() - 1])) < 1e-12)
|
||||
.count();
|
||||
assert_eq!(
|
||||
meridians + rounds,
|
||||
8,
|
||||
"{meridians} meridians, {rounds} round"
|
||||
);
|
||||
assert!(meridians > 0 && rounds > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_half_spheres_lines_stay_on_its_half() {
|
||||
let (b, size) = (fixture("smlib_half_sphere"), size_of("smlib_half_sphere"));
|
||||
let tol = 0.01;
|
||||
let lines = isoparms(&b, 4, 4, MeshTolerance::chord(tol), size).unwrap();
|
||||
let on = sphere_lines(&b, &lines, tol);
|
||||
assert!(!on.is_empty());
|
||||
// every point of the sphere's lines on one side of the rim's plane z = 0
|
||||
let zs: Vec<f64> = on.iter().flatten().map(|p| p[2]).collect();
|
||||
assert!(
|
||||
zs.iter().all(|z| *z >= -1e-9) || zs.iter().all(|z| *z <= 1e-9),
|
||||
"both sides of the rim"
|
||||
);
|
||||
// and a meridian reaches the pole
|
||||
assert!(on.iter().flatten().any(|p| p[0].hypot(p[1]) < 1e-6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pocketed_spheres_lines_lie_on_it() {
|
||||
let (b, size) = (
|
||||
fixture("sphere_r10_with_pocket"),
|
||||
size_of("sphere_r10_with_pocket"),
|
||||
);
|
||||
let tol = 0.01;
|
||||
let lines = isoparms(&b, 6, 6, MeshTolerance::chord(tol), size).unwrap();
|
||||
let on = sphere_lines(&b, &lines, tol);
|
||||
assert!(on.len() >= 6, "{}", on.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tolerance_is_the_plans_for_the_bodys_size() {
|
||||
// a chord of 1e-9 on a body a million across is the plan's millionth of it: 1
|
||||
let b = fixture("cylinder_r10_h5");
|
||||
let fine = isoparms(&b, 0, 2, MeshTolerance::chord(1e-3), 0.0).unwrap();
|
||||
let coarse = isoparms(&b, 0, 2, MeshTolerance::chord(1e-9), 1e6).unwrap();
|
||||
let count = |ls: &[Vec<[f64; 3]>]| ls.iter().map(Vec::len).sum::<usize>();
|
||||
assert!(
|
||||
count(&coarse) < count(&fine),
|
||||
"{} {}",
|
||||
count(&coarse),
|
||||
count(&fine)
|
||||
);
|
||||
let side = b
|
||||
.faces
|
||||
.iter()
|
||||
.find(|f| matches!(f.surface, Surface::Cylinder { .. }))
|
||||
.unwrap();
|
||||
let on_side = |p: &[f64; 3]| {
|
||||
let (r, radius) = radial(&side.surface, *p).unwrap();
|
||||
(r - radius).abs() <= 1.0001
|
||||
};
|
||||
for l in coarse.iter().filter(|l| l.iter().all(on_side)) {
|
||||
for w in l.windows(2) {
|
||||
let m = [0, 1, 2].map(|i| (w[0][i] + w[1][i]) / 2.0);
|
||||
let (r, radius) = radial(&side.surface, m).unwrap();
|
||||
assert!(radius - r <= 1.0001, "sag {}", radius - r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `p` lies on a triangle of `mesh` within `eps`: in its plane and inside it.
|
||||
fn on_mesh(mesh: &crate::Mesh, p: [f64; 3], eps: f64) -> bool {
|
||||
mesh.triangles.iter().any(|t| {
|
||||
let [a, b, c] = t.map(|i| {
|
||||
let v = mesh.positions[i as usize];
|
||||
[v.x, v.y, v.z]
|
||||
});
|
||||
let (ab, ac, ap) = (sub(b, a), sub(c, a), sub(p, a));
|
||||
let n = cross(ab, ac);
|
||||
let area = dot(n, n).sqrt();
|
||||
if area == 0.0 || dot(ap, n).abs() > eps * area {
|
||||
return false;
|
||||
}
|
||||
// the barycentric coordinates of p, within eps of the triangle's sides
|
||||
let (d00, d01, d11) = (dot(ab, ab), dot(ab, ac), dot(ac, ac));
|
||||
let (d20, d21) = (dot(ap, ab), dot(ap, ac));
|
||||
let den = d00 * d11 - d01 * d01;
|
||||
let v = (d11 * d20 - d01 * d21) / den;
|
||||
let w = (d00 * d21 - d01 * d20) / den;
|
||||
let slack = eps * (d00.max(d11)).sqrt() / area;
|
||||
v >= -slack && w >= -slack && v + w <= 1.0 + slack
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_concave_necks_lines_lie_on_its_triangles() {
|
||||
// the inside of a torus is a neck: concave along its tube's circles, where the true
|
||||
// surface lies behind the chords; every isoparm point on a displayed triangle
|
||||
let (b, size) = (fixture("torus_22_4"), size_of("torus_22_4"));
|
||||
let t = MeshTolerance::display_of(size);
|
||||
let lines = isoparms(&b, 6, 6, t, size).unwrap();
|
||||
assert!(lines.len() >= 12, "{}", lines.len());
|
||||
let (mesh, _) = plan(&b, &t, size).unwrap().0.evaluate();
|
||||
for l in &lines {
|
||||
for p in l {
|
||||
assert!(on_mesh(&mesh, *p, 1e-9), "{p:?} off the mesh");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -23,6 +23,7 @@
|
||||
mod curve;
|
||||
#[cfg(test)]
|
||||
mod fixtures;
|
||||
mod iso;
|
||||
mod mesh;
|
||||
// SolidCraft's mesher, moved here (RFC 0003, item 1.1) and kept in SolidCraft's formatting so it
|
||||
// reads the same as its history.
|
||||
@@ -39,6 +40,7 @@ mod tolerance;
|
||||
mod vec;
|
||||
|
||||
pub use curve::curve_points;
|
||||
pub use iso::{MAX_ISOPARMS, isoparms};
|
||||
pub use mesh::Mesh;
|
||||
pub use mesher::{MAX_ANGLE_BOUNDARY, plan};
|
||||
pub use par::guarded;
|
||||
|
||||
+149
-86
@@ -179,10 +179,109 @@ pub fn plan(b: &brep::Brep, t: &crate::MeshTolerance, size: f64) -> Result<(Tess
|
||||
plan_at(b, tol, turn)
|
||||
}
|
||||
|
||||
/// What the planner reads of a body before it plans any face, in the record's order: each face's
|
||||
/// surface (in the evaluator's parameters), target size and boundary loops in (u, v), every
|
||||
/// edge's shared nodes, and why each face left out is. [`plan_at`] plans the faces from it.
|
||||
pub(crate) struct Prepared {
|
||||
/// The rules every edge was sampled to.
|
||||
pub(crate) rules: size::Rules,
|
||||
/// Whether each face's normal is its surface's natural normal.
|
||||
pub(crate) forward: Vec<bool>,
|
||||
/// The faces each edge bounds.
|
||||
pub(crate) edge_faces: Vec<Vec<usize>>,
|
||||
/// Each face's surface (a plane in the place of a face left out).
|
||||
pub(crate) evals: Vec<poles::Face>,
|
||||
/// Each face's target side length and its surface's mean speed along u and v.
|
||||
pub(crate) sizes: Vec<(f64, [f64; 2])>,
|
||||
/// Every edge's shared nodes.
|
||||
pub(crate) nodes: Vec<edges::Nodes>,
|
||||
/// The edges that bound no face and could not be sampled.
|
||||
pub(crate) unsampled_edges: Vec<crate::RefusedEdge>,
|
||||
/// The edges kept to the strict rules though their faces are crowded.
|
||||
pub(crate) kept_strict: Vec<crate::RefusedEdge>,
|
||||
boundaries: Vec<Boundary>,
|
||||
/// Why each face is left out, where it is.
|
||||
pub(crate) why: Vec<Option<TessError>>,
|
||||
/// The faces whose boundary still crosses itself, and why.
|
||||
pub(crate) tangled: Vec<(usize, String)>,
|
||||
}
|
||||
|
||||
/// Plan the tessellation of `b` with chord tolerance `tol` and, when given, a facet angle no
|
||||
/// facet may turn more than (where it sags above the angle's floor). Faces and edges are the
|
||||
/// record's, in its order.
|
||||
pub(crate) fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<(TessPlan, PlanStats)> {
|
||||
let faces = &b.faces;
|
||||
let Prepared { rules, forward, edge_faces, evals, sizes, nodes, unsampled_edges, kept_strict, boundaries, why, tangled } = prepare(b, tol, turn)?;
|
||||
let curved = evals.iter().filter(|e| !matches!(***e, EvalSurface::Plane { .. })).count();
|
||||
let indexed: Vec<usize> = (0..faces.len()).collect();
|
||||
let planned = crate::par::map("face tessellation", &indexed, curved >= PARALLEL_FACES, |&fi| {
|
||||
if let Some(Some(e)) = why.get(fi) {
|
||||
return Err(e.clone());
|
||||
}
|
||||
let (Some(face), Some(eval), Some(size), Some(fb)) = (faces.get(fi), evals.get(fi), sizes.get(fi), boundaries.get(fi)) else {
|
||||
return Err(unsupported(format!("face {fi}: missing")));
|
||||
};
|
||||
let refused = tangled.iter().find(|(f, _)| *f == fi).map(|(_, why)| why.as_str());
|
||||
let result = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size, fb.reach, refused);
|
||||
// A point whose poles the kernel could not tell refuses the face: nothing is taken for
|
||||
// its answer. Read before the plan's own error, which may be a later symptom of it.
|
||||
if let Some(why) = eval.refusal() {
|
||||
return Err(unsupported(format!("face {fi} ({}): {why}", kind(&face.surface))));
|
||||
}
|
||||
let (fp, stats) = result?;
|
||||
let fwd = forward.get(fi).copied().unwrap_or(true);
|
||||
let fp =
|
||||
FacePlan { flip: !fwd, triangles: if fwd { fp.triangles } else { fp.triangles.into_iter().map(|[a, b, c]| [a, c, b]).collect() }, ..fp };
|
||||
// Its poles of higher order and the normals they get, on the oriented triangles, by the
|
||||
// kernel's evaluator ([`crate::pole`]).
|
||||
let (poles, averaged) = crate::pole::plan_poles(fi, eval.kernel(), &fp, fb.reach);
|
||||
let mut stats = stats;
|
||||
stats.averaged_poles = averaged;
|
||||
Ok((FacePlan { poles, ..fp }, stats))
|
||||
});
|
||||
let mut plan = TessPlan::default();
|
||||
let mut stats = PlanStats { unsampled_edges, kept_strict, ..PlanStats::default() };
|
||||
// A face no method plans is left out, named in the stats, and the rest of the body is
|
||||
// meshed; a body none of whose faces plan is the first face's error.
|
||||
let mut first_error: Option<TessError> = None;
|
||||
for (fi, (r, eval)) in planned.into_iter().zip(evals.into_iter().map(poles::Face::into_eval)).enumerate() {
|
||||
let surface = u32::try_from(plan.surfaces.len()).map_err(|_| unsupported("too many faces"))?;
|
||||
plan.surfaces.push(eval);
|
||||
let (fp, s) = match r {
|
||||
Ok(done) => done,
|
||||
Err(e) => {
|
||||
stats.unmeshed.push(RefusedFace { face: fi, reason: e.to_string() });
|
||||
first_error.get_or_insert(e);
|
||||
(FacePlan { face: u32::try_from(fi).unwrap_or(u32::MAX), ..FacePlan::default() }, PlanStats::default())
|
||||
}
|
||||
};
|
||||
plan.faces.push(FacePlan { surface: Some(surface), ..fp });
|
||||
stats.planar += s.planar;
|
||||
stats.structured += s.structured;
|
||||
stats.delaunay += s.delaunay;
|
||||
stats.lenient.extend(s.lenient);
|
||||
stats.averaged_poles.extend(s.averaged_poles);
|
||||
stats.over_tolerance.extend(s.over_tolerance);
|
||||
}
|
||||
if let Some(e) = first_error
|
||||
&& stats.unmeshed.len() == faces.len()
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
// Edges a bound of their sampling stopped, named with how far their chords lie off them.
|
||||
for (edge, n) in nodes.iter().enumerate() {
|
||||
if let Some((cap, sag, excess)) = n.capped {
|
||||
stats.edges_capped.push(crate::CappedEdge { edge, cap, sag, excess, tol: rules.tol, within: sag <= rules.tol });
|
||||
}
|
||||
}
|
||||
plan.edges = nodes.into_iter().map(|n| n.points.into_iter().map(kernel::record_vec3).collect()).collect();
|
||||
plan.edge_faces = edge_faces.into_iter().map(|fs| fs.into_iter().map(|f| u32::try_from(f).unwrap_or(u32::MAX)).collect()).collect();
|
||||
Ok((plan, stats))
|
||||
}
|
||||
|
||||
/// Every face's surface, size and boundary and every edge's nodes, as [`plan_at`] reads them
|
||||
/// ([`Prepared`]), with chord tolerance `tol` and the facet angle `turn`.
|
||||
pub(crate) fn prepare(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result<Prepared> {
|
||||
let faces = &b.faces;
|
||||
let forward = forward_faces(b);
|
||||
// The faces each edge bounds, each once, in the order of the edge's uses.
|
||||
@@ -310,69 +409,7 @@ pub(crate) fn plan_at(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Res
|
||||
});
|
||||
}
|
||||
let tangled = untangle(b, &evals, &edge_faces, rules.tol, &mut nodes, &mut boundaries, &face_boundary, &mut why)?;
|
||||
let planned = crate::par::map("face tessellation", &indexed, curved >= PARALLEL_FACES, |&fi| {
|
||||
if let Some(Some(e)) = why.get(fi) {
|
||||
return Err(e.clone());
|
||||
}
|
||||
let (Some(face), Some(eval), Some(size), Some(fb)) = (faces.get(fi), evals.get(fi), sizes.get(fi), boundaries.get(fi)) else {
|
||||
return Err(unsupported(format!("face {fi}: missing")));
|
||||
};
|
||||
let refused = tangled.iter().find(|(f, _)| *f == fi).map(|(_, why)| why.as_str());
|
||||
let result = plan_or_fill(fi, kind(face.parameters()), eval, &fb.loops, &rules, *size, fb.reach, refused);
|
||||
// A point whose poles the kernel could not tell refuses the face: nothing is taken for
|
||||
// its answer. Read before the plan's own error, which may be a later symptom of it.
|
||||
if let Some(why) = eval.refusal() {
|
||||
return Err(unsupported(format!("face {fi} ({}): {why}", kind(&face.surface))));
|
||||
}
|
||||
let (fp, stats) = result?;
|
||||
let fwd = forward.get(fi).copied().unwrap_or(true);
|
||||
let fp =
|
||||
FacePlan { flip: !fwd, triangles: if fwd { fp.triangles } else { fp.triangles.into_iter().map(|[a, b, c]| [a, c, b]).collect() }, ..fp };
|
||||
// Its poles of higher order and the normals they get, on the oriented triangles, by the
|
||||
// kernel's evaluator ([`crate::pole`]).
|
||||
let (poles, averaged) = crate::pole::plan_poles(fi, eval.kernel(), &fp, fb.reach);
|
||||
let mut stats = stats;
|
||||
stats.averaged_poles = averaged;
|
||||
Ok((FacePlan { poles, ..fp }, stats))
|
||||
});
|
||||
let mut plan = TessPlan::default();
|
||||
let mut stats = PlanStats { unsampled_edges, kept_strict, ..PlanStats::default() };
|
||||
// A face no method plans is left out, named in the stats, and the rest of the body is
|
||||
// meshed; a body none of whose faces plan is the first face's error.
|
||||
let mut first_error: Option<TessError> = None;
|
||||
for (fi, (r, eval)) in planned.into_iter().zip(evals.into_iter().map(poles::Face::into_eval)).enumerate() {
|
||||
let surface = u32::try_from(plan.surfaces.len()).map_err(|_| unsupported("too many faces"))?;
|
||||
plan.surfaces.push(eval);
|
||||
let (fp, s) = match r {
|
||||
Ok(done) => done,
|
||||
Err(e) => {
|
||||
stats.unmeshed.push(RefusedFace { face: fi, reason: e.to_string() });
|
||||
first_error.get_or_insert(e);
|
||||
(FacePlan { face: u32::try_from(fi).unwrap_or(u32::MAX), ..FacePlan::default() }, PlanStats::default())
|
||||
}
|
||||
};
|
||||
plan.faces.push(FacePlan { surface: Some(surface), ..fp });
|
||||
stats.planar += s.planar;
|
||||
stats.structured += s.structured;
|
||||
stats.delaunay += s.delaunay;
|
||||
stats.lenient.extend(s.lenient);
|
||||
stats.averaged_poles.extend(s.averaged_poles);
|
||||
stats.over_tolerance.extend(s.over_tolerance);
|
||||
}
|
||||
if let Some(e) = first_error
|
||||
&& stats.unmeshed.len() == faces.len()
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
// Edges a bound of their sampling stopped, named with how far their chords lie off them.
|
||||
for (edge, n) in nodes.iter().enumerate() {
|
||||
if let Some((cap, sag, excess)) = n.capped {
|
||||
stats.edges_capped.push(crate::CappedEdge { edge, cap, sag, excess, tol: rules.tol, within: sag <= rules.tol });
|
||||
}
|
||||
}
|
||||
plan.edges = nodes.into_iter().map(|n| n.points.into_iter().map(kernel::record_vec3).collect()).collect();
|
||||
plan.edge_faces = edge_faces.into_iter().map(|fs| fs.into_iter().map(|f| u32::try_from(f).unwrap_or(u32::MAX)).collect()).collect();
|
||||
Ok((plan, stats))
|
||||
Ok(Prepared { rules, forward, edge_faces, evals, sizes, nodes, unsampled_edges, kept_strict, boundaries, why, tangled })
|
||||
}
|
||||
|
||||
/// Face `fi`'s boundary, loop by loop: the shared edge nodes (each edge's last node is the next
|
||||
@@ -1153,28 +1190,29 @@ fn fan(range: std::ops::Range<usize>, uvs: &[[f64; 2]], same: &dyn Fn(u32, u32)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One face: boundary loops in 3D (shared edge nodes) to parameters and triangles.
|
||||
/// `size`: the face's target side length (mm) and the surface's mean speed along u and v.
|
||||
/// `reach`: how near a pole a point is taken as one ([`Boundary::reach`]). `lenient`: the last resort
|
||||
/// for a face no method could plan (see [`Lenient`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn plan_face(
|
||||
fi: usize,
|
||||
kind: &str,
|
||||
s: &poles::Face,
|
||||
loops: &[seam::Loop],
|
||||
rules: &size::Rules,
|
||||
size: (f64, [f64; 2]),
|
||||
reach: f64,
|
||||
stats: &mut PlanStats,
|
||||
lenient: Lenient,
|
||||
) -> Result<FacePlan> {
|
||||
let tol = rules.tol;
|
||||
let (h, speed) = size;
|
||||
// The memory bounds that limit this face, if any ([`crate::Cap`]).
|
||||
let mut caps: Vec<crate::Cap> = Vec::new();
|
||||
/// A face's boundary loops as closed polygons in its surface's (u, v), as the planner
|
||||
/// triangulates them: every node in order, the
|
||||
/// 3D point each stands for, and each loop's range.
|
||||
pub(crate) struct Polygons {
|
||||
/// Each node's (u, v).
|
||||
pub(crate) uvs: Vec<[f64; 2]>,
|
||||
/// Each node's 3D point (the shared edge node it stands for).
|
||||
pub(crate) fixed: Vec<Vec3>,
|
||||
/// Each loop's nodes.
|
||||
pub(crate) ranges: Vec<std::ops::Range<usize>>,
|
||||
/// Whether the face was cut along a seam into one period.
|
||||
pub(crate) seamed: bool,
|
||||
}
|
||||
|
||||
/// Face loops `loops` on `s` made closed polygons in (u, v): a loop
|
||||
/// up a seam to a pole and back down it opened at the pole; a face whose boundary winds round a
|
||||
/// closed direction cut along a parameter line into one period ([`seam::cut`], at side `h`); a
|
||||
/// loop from pole to pole and back along one seam opened, and holes moved by whole periods
|
||||
/// beside the outer loop; and at a pole (within `reach`, [`Boundary::reach`]) the free parameter
|
||||
/// split into the previous node's value and the next one's, so the polygon runs along the
|
||||
/// collapsed side. Why not, where the boundary winds round the surface and cannot be cut.
|
||||
pub(crate) fn polygons(s: &poles::Face, loops: &[seam::Loop], rules: &size::Rules, h: f64, reach: f64) -> std::result::Result<Polygons, &'static str> {
|
||||
let dom = uv::domain(s);
|
||||
let fail = |why: &str| unsupported(format!("face {fi} ({kind}): {why}"));
|
||||
let mut projected: Vec<seam::Loop> = loops.to_vec();
|
||||
// A loop that winds around a periodic direction does not close in (u, v): the face is cut
|
||||
// along a parameter line into one period (a seamless cylinder face, a helical trim).
|
||||
@@ -1192,9 +1230,9 @@ fn plan_face(
|
||||
[] => {}
|
||||
[d] => {
|
||||
let per = dom.period.get(*d).copied().flatten().unwrap_or(std::f64::consts::TAU);
|
||||
projected = seam::cut(s, &projected, *d, per, rules, h).ok_or_else(|| fail("boundary winds around the surface"))?;
|
||||
projected = seam::cut(s, &projected, *d, per, rules, h).ok_or("boundary winds around the surface")?;
|
||||
}
|
||||
_ => return Err(fail("boundary winds around the surface both ways")),
|
||||
_ => return Err("boundary winds around the surface both ways"),
|
||||
}
|
||||
// A loop from pole to pole and back along one seam (a whole sphere as one face, as another
|
||||
// kernel writes it) projects onto one parameter line: its second run is a period along.
|
||||
@@ -1241,6 +1279,31 @@ fn plan_face(
|
||||
}
|
||||
ranges.push(start..uvs.len());
|
||||
}
|
||||
Ok(Polygons { uvs, fixed, ranges, seamed })
|
||||
}
|
||||
|
||||
/// One face: boundary loops in 3D (shared edge nodes) to parameters and triangles.
|
||||
/// `size`: the face's target side length (mm) and the surface's mean speed along u and v.
|
||||
/// `reach`: how near a pole a point is taken as one ([`Boundary::reach`]). `lenient`: the last resort
|
||||
/// for a face no method could plan (see [`Lenient`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn plan_face(
|
||||
fi: usize,
|
||||
kind: &str,
|
||||
s: &poles::Face,
|
||||
loops: &[seam::Loop],
|
||||
rules: &size::Rules,
|
||||
size: (f64, [f64; 2]),
|
||||
reach: f64,
|
||||
stats: &mut PlanStats,
|
||||
lenient: Lenient,
|
||||
) -> Result<FacePlan> {
|
||||
let tol = rules.tol;
|
||||
let (h, speed) = size;
|
||||
// The memory bounds that limit this face, if any ([`crate::Cap`]).
|
||||
let mut caps: Vec<crate::Cap> = Vec::new();
|
||||
let fail = |why: &str| unsupported(format!("face {fi} ({kind}): {why}"));
|
||||
let Polygons { uvs, fixed, ranges, seamed } = polygons(s, loops, rules, h, reach).map_err(fail)?;
|
||||
if uvs.len() < 3 {
|
||||
return Err(fail("degenerate boundary"));
|
||||
}
|
||||
|
||||
+70
-41
@@ -59,7 +59,71 @@ pub struct TessPlan {
|
||||
pub edge_faces: Vec<Vec<u32>>,
|
||||
}
|
||||
|
||||
/// One face's nodes as the mesh has them ([`TessPlan::nodes`]).
|
||||
pub(crate) struct FaceNodes {
|
||||
/// Each node's position.
|
||||
pub(crate) positions: Vec<Vec3>,
|
||||
/// Each node's normal (turned with the face).
|
||||
pub(crate) normals: Vec<Vec3>,
|
||||
/// The kernel's form of the face's surface, where a node needed it.
|
||||
pub(crate) kernel: Option<solidcore::eval::EvalSurface>,
|
||||
/// The first node the kernel's evaluator refuses, and why.
|
||||
pub(crate) failure: Option<String>,
|
||||
}
|
||||
|
||||
impl TessPlan {
|
||||
/// Face `f`'s node positions and normals as [`TessPlan::evaluate`] gives them, before its
|
||||
/// pole rules: each node evaluated on the face's surface (a shared edge node's own point
|
||||
/// kept), the kernel's form of the surface where a node needed it, and the first node the
|
||||
/// kernel's evaluator refuses, if one does (the face is then left out of the mesh).
|
||||
pub(crate) fn nodes(&self, f: &FacePlan) -> FaceNodes {
|
||||
let surface = f.surface.and_then(|i| self.surfaces.get(i as usize));
|
||||
// At a degenerate point (a cone's apex, a sphere's pole) the kernel's exact limit
|
||||
// normal, from the second partials; the kernel's form of the face is made only when a
|
||||
// vertex of it needs one.
|
||||
let mut kernel: Option<solidcore::eval::EvalSurface> = None;
|
||||
let (mut positions, mut normals) = (
|
||||
Vec::with_capacity(f.uv.len()),
|
||||
Vec::with_capacity(f.uv.len()),
|
||||
);
|
||||
// The first node the kernel's evaluator refuses, with its reason: the face is refused
|
||||
// there, not meshed with a normal made up for it. Its pole nodes have their own rule
|
||||
// in [`TessPlan::evaluate`] ([`crate::pole::apply`]).
|
||||
let mut failure: Option<String> = None;
|
||||
for (k, uv) in f.uv.iter().enumerate() {
|
||||
let (p, n) = match surface {
|
||||
Some(s) => {
|
||||
solidcore_convert::record_pair(if let Some(pn) = s.regular(uv[0], uv[1]) {
|
||||
pn
|
||||
} else {
|
||||
let ks = kernel.get_or_insert_with(|| crate::mesher::poles::kernel_form(s));
|
||||
ks.eval(uv[0], uv[1]).unwrap_or_else(|e| {
|
||||
if failure.is_none() && !f.poles.iter().any(|p| p.node as usize == k) {
|
||||
failure = Some(format!(
|
||||
"node {k} at ({}, {}): {}",
|
||||
uv[0],
|
||||
uv[1],
|
||||
crate::eval_message(e)
|
||||
));
|
||||
}
|
||||
s.eval(uv[0], uv[1])
|
||||
})
|
||||
})
|
||||
}
|
||||
None => (Vec3::ZERO, f.normals.get(k).copied().unwrap_or(Vec3::Z)),
|
||||
};
|
||||
let p = f.fixed.get(k).copied().unwrap_or(p);
|
||||
positions.push(p);
|
||||
normals.push(if f.flip { -n } else { n });
|
||||
}
|
||||
FaceNodes {
|
||||
positions,
|
||||
normals,
|
||||
kernel,
|
||||
failure,
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate on the CPU into an indexed mesh (vertices are shared within a face, not
|
||||
/// across faces, so creases keep their own normals), with the faces left out of it.
|
||||
#[must_use]
|
||||
@@ -69,47 +133,12 @@ impl TessPlan {
|
||||
for f in &self.faces {
|
||||
let base = out.positions.len();
|
||||
let surface = f.surface.and_then(|i| self.surfaces.get(i as usize));
|
||||
// At a degenerate point (a cone's apex, a sphere's pole) the kernel's exact limit
|
||||
// normal, from the second partials; the kernel's form of the face is made only when a
|
||||
// vertex of it needs one.
|
||||
let mut kernel: Option<solidcore::eval::EvalSurface> = None;
|
||||
let (mut positions, mut normals) = (
|
||||
Vec::with_capacity(f.uv.len()),
|
||||
Vec::with_capacity(f.uv.len()),
|
||||
);
|
||||
// The first node the kernel's evaluator refuses, with its reason: the face is refused
|
||||
// there, not meshed with a normal made up for it. Its pole nodes have their own rule
|
||||
// below ([`crate::pole::apply`]).
|
||||
let mut failure: Option<String> = None;
|
||||
for (k, uv) in f.uv.iter().enumerate() {
|
||||
let (p, n) = match surface {
|
||||
Some(s) => {
|
||||
solidcore_convert::record_pair(if let Some(pn) = s.regular(uv[0], uv[1]) {
|
||||
pn
|
||||
} else {
|
||||
let ks =
|
||||
kernel.get_or_insert_with(|| crate::mesher::poles::kernel_form(s));
|
||||
ks.eval(uv[0], uv[1]).unwrap_or_else(|e| {
|
||||
if failure.is_none()
|
||||
&& !f.poles.iter().any(|p| p.node as usize == k)
|
||||
{
|
||||
failure = Some(format!(
|
||||
"node {k} at ({}, {}): {}",
|
||||
uv[0],
|
||||
uv[1],
|
||||
crate::eval_message(e)
|
||||
));
|
||||
}
|
||||
s.eval(uv[0], uv[1])
|
||||
})
|
||||
})
|
||||
}
|
||||
None => (Vec3::ZERO, f.normals.get(k).copied().unwrap_or(Vec3::Z)),
|
||||
};
|
||||
let p = f.fixed.get(k).copied().unwrap_or(p);
|
||||
positions.push(p);
|
||||
normals.push(if f.flip { -n } else { n });
|
||||
}
|
||||
let FaceNodes {
|
||||
mut positions,
|
||||
mut normals,
|
||||
mut kernel,
|
||||
failure,
|
||||
} = self.nodes(f);
|
||||
if let Some(reason) = failure {
|
||||
refused.push(crate::RefusedFace {
|
||||
face: f.face as usize,
|
||||
|
||||
+38
-8
@@ -263,12 +263,16 @@ pub fn classify(
|
||||
tangency_tol_deg: f64,
|
||||
) -> Result<BlendCorner> {
|
||||
let mut out = BlendCorner::default();
|
||||
let vertex = vertex.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?;
|
||||
let vertex =
|
||||
vertex.ok_or_else(|| SolidError::blend_missing(OP_CLASSIFY, "the corner's vertex"))?;
|
||||
if solver_eus.len() != 2 || (all_eus.len() != 3 && all_eus.len() != 4) {
|
||||
return Ok(out);
|
||||
}
|
||||
let (Some(&eu1_), Some(&eu2_)) = (solver_eus.first(), solver_eus.get(1)) else {
|
||||
return Err(SolidError::null_pointer(OP_CLASSIFY));
|
||||
return Err(SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"the corner's two blended edge uses",
|
||||
));
|
||||
};
|
||||
// a Join: the blends continue each other, or, at a face corner of the other
|
||||
// convexity, meet at 5 degrees or less (they blend there: no mitre so thin)
|
||||
@@ -316,7 +320,9 @@ fn side_edges_tangent(
|
||||
if blended.contains(&e) {
|
||||
continue;
|
||||
}
|
||||
let e = e.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?;
|
||||
let e = e.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_CLASSIFY, "an unblended edge use's edge")
|
||||
})?;
|
||||
if !edge::is_tangent_edge(k, b, e, tol_deg, false)? {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -353,10 +359,9 @@ fn foot_on_surface(
|
||||
for _ in 0..40 {
|
||||
let d = surface.evaluate(k, uv, 1, 1, flags)?;
|
||||
let at = |i: usize, j: usize| {
|
||||
d.get(i)
|
||||
.and_then(|row| row.get(j))
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXACT))
|
||||
d.get(i).and_then(|row| row.get(j)).copied().ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXACT, "a derivative of the surface's evaluation")
|
||||
})
|
||||
};
|
||||
let r = point - at(0, 0)?;
|
||||
let su = at(1, 0)?;
|
||||
@@ -449,7 +454,12 @@ pub fn exact_mitre_curve(
|
||||
mitre: EdgeId,
|
||||
solvers: &mut dyn MitreSolvers,
|
||||
) -> Result<bool> {
|
||||
let nul = || SolidError::null_pointer(OP_EXACT);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_EXACT,
|
||||
"topology or geometry the mitre curve is built from",
|
||||
)
|
||||
};
|
||||
let start = edge::vertex(pseudo.brep(), mitre)?.ok_or_else(nul)?;
|
||||
let end = edge::other_vertex(pseudo.brep(), mitre, Some(start))?.ok_or_else(nul)?;
|
||||
let from_s = pseudo.fillet_vertex(start)?.vertex_type == FilletVertexType::RailXEdgeuse;
|
||||
@@ -831,6 +841,26 @@ pub fn exact_mitre_curve(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A classification without a vertex is a typed `Missing` failure, with the null-pointer
|
||||
/// status the bare status had.
|
||||
#[test]
|
||||
fn a_corner_without_a_vertex_is_a_typed_missing_failure() {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = crate::primitive::api_primitives::create_box(&mut k, Vec3::ZERO, 1.0, 1.0, 1.0)
|
||||
.unwrap();
|
||||
let err = classify(&k, &mut b, None, &[], &[], 1.0).unwrap_err();
|
||||
assert_eq!(
|
||||
err.blend_failure(),
|
||||
Some(&crate::error::blend::BlendFailure::Missing(
|
||||
"the corner's vertex"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&err),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bernstein_product_is_the_cubic() {
|
||||
// (1 + t)^2 * (2 - t): Bernstein (1, 2, 4) x (2, 1)
|
||||
|
||||
+71
-15
@@ -183,7 +183,10 @@ fn planes_meet(on: &[Point3; 3], normal: &[Vec3; 3]) -> Result<Point3> {
|
||||
let [n0, n1, n2] = *normal;
|
||||
let det = n0.dot(n1.cross(n2));
|
||||
if det.abs() <= EFF_ZERO {
|
||||
return Err(SolidError::sm_err(OP_CLASSIFY));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_CLASSIFY,
|
||||
"the three faces' planes do not meet in one point",
|
||||
));
|
||||
}
|
||||
let mut rhs = [0.0; 3];
|
||||
for ((r, n), p) in rhs.iter_mut().zip(normal.iter()).zip(on.iter()) {
|
||||
@@ -207,7 +210,8 @@ fn max(a: f64, b: f64) -> f64 {
|
||||
/// `sm_BallFaceQuadric` (the fork's 7048946): the quadric (a plane or a natural quadric) a
|
||||
/// ring face lies on, [`QuadricKind::None`] where it lies on none.
|
||||
fn ball_face_quadric(k: &Kernel, b: &Brep, face: FaceId, tol: f64) -> Result<BlendQuadric> {
|
||||
let surface = face2::surface(b, face)?.ok_or_else(|| SolidError::null_pointer(OP_QUADRIC))?;
|
||||
let surface = face2::surface(b, face)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_QUADRIC, "the ring face's surface"))?;
|
||||
let q = BlendQuadric::from_surface(k, surface, face2::uv_domain(b, face)?.mid(), tol)?;
|
||||
if !matches!(
|
||||
q.kind,
|
||||
@@ -390,7 +394,7 @@ fn ball_certified_root(f: &[BallImplicit; 3], half: f64) -> ([f64; 3], bool) {
|
||||
fn offset_radius(solver: &FilletSolver, rail: usize, op: &'static str) -> Result<f64> {
|
||||
let slot = solver
|
||||
.surface(rail)?
|
||||
.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the rail's offset surface"))?;
|
||||
Ok(slot.surface.offset_distance().abs())
|
||||
}
|
||||
|
||||
@@ -419,7 +423,12 @@ pub fn classify_ball(
|
||||
set_back: bool,
|
||||
solvers: &dyn BallSolvers,
|
||||
) -> Result<BlendCornerEvent> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"topology or a solver the ball corner's classification reads",
|
||||
)
|
||||
};
|
||||
let none = Ok(BlendCornerEvent::None);
|
||||
let n = all_eus.len();
|
||||
if n < 3 || solver_eus.len() != n || set_back {
|
||||
@@ -553,8 +562,13 @@ fn ball_centre(
|
||||
corner: &FilletCorner,
|
||||
solvers: &dyn BallSolvers,
|
||||
) -> Result<(Point3, f64, Vec<Point3>)> {
|
||||
let nul = || SolidError::null_pointer(OP_CENTRE);
|
||||
let err = || SolidError::sm_err(OP_CENTRE);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CENTRE,
|
||||
"topology or a solver the ball's centre is found from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_CENTRE, "the ball's centre could not be found");
|
||||
let all_eus = corner.all_eus.as_deref().ok_or_else(nul)?;
|
||||
let n = all_eus.len();
|
||||
if n < 3 || corner.vertices.len() != n || corner.edges.len() != n {
|
||||
@@ -800,8 +814,13 @@ pub fn place_ball_contacts(
|
||||
corner: &FilletCorner,
|
||||
solvers: &dyn BallSolvers,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_PLACE);
|
||||
let err = || SolidError::sm_err(OP_PLACE);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_PLACE,
|
||||
"topology or a solver the ball's contacts are placed from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_PLACE, "the ball's contacts could not be placed");
|
||||
let (centre, _radius, contacts) = ball_centre(k, target, pseudo, corner, solvers)?;
|
||||
let tol = corner.approx_tol_3d;
|
||||
for (&fv, &contact) in corner.vertices.iter().zip(contacts.iter()) {
|
||||
@@ -869,7 +888,9 @@ impl BallPoly {
|
||||
.c
|
||||
.get_mut(i)
|
||||
.and_then(|row| row.get_mut(j))
|
||||
.ok_or_else(|| SolidError::sm_err(OP_PATCH_NET))?;
|
||||
.ok_or_else(|| {
|
||||
SolidError::blend_failed(OP_PATCH_NET, "a control point past the patch's net")
|
||||
})?;
|
||||
*slot = v;
|
||||
Ok(())
|
||||
}
|
||||
@@ -939,7 +960,12 @@ pub(crate) fn ball_triangle_patch(
|
||||
pole: usize,
|
||||
reverse_s: bool,
|
||||
) -> Result<(BSplineSurface, usize, usize)> {
|
||||
let err = || SolidError::sm_err(OP_PATCH_NET);
|
||||
let err = || {
|
||||
SolidError::blend_failed(
|
||||
OP_PATCH_NET,
|
||||
"the ball's triangular patch could not be built",
|
||||
)
|
||||
};
|
||||
let mut from = (pole + 1) % 3;
|
||||
let mut to = (pole + 2) % 3;
|
||||
let (Some(&d_pole), Some(&d_from), Some(&d_to)) =
|
||||
@@ -1073,8 +1099,13 @@ pub fn ball_corner_edge_geom(
|
||||
corner: &mut FilletCorner,
|
||||
solvers: &dyn BallSolvers,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_BUILD);
|
||||
let err = || SolidError::sm_err(OP_BUILD);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_BUILD,
|
||||
"topology or geometry the ball corner's edges are built from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_BUILD, "the ball corner's edges could not be built");
|
||||
corner.ball.as_mut().ok_or_else(nul)?.delete_surface();
|
||||
let (centre, radius, contacts) = ball_centre(k, target, pseudo, corner, solvers)?;
|
||||
let tol = corner.approx_tol_3d;
|
||||
@@ -1193,10 +1224,13 @@ pub fn create_ball_patch(
|
||||
corner: &mut FilletCorner,
|
||||
env: &mut CornerPatchEnv<'_, '_>,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_PATCH);
|
||||
let nul = || SolidError::blend_missing(OP_PATCH, "the ball corner's event or topology");
|
||||
let ball = corner.ball.as_mut().ok_or_else(nul)?;
|
||||
if ball.surfaces.is_empty() || ball.loops.len() != ball.surfaces.len() {
|
||||
return Err(SolidError::sm_err(OP_PATCH));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_PATCH,
|
||||
"the ball's surfaces and loops do not pair up",
|
||||
));
|
||||
}
|
||||
let surfaces = std::mem::take(&mut ball.surfaces);
|
||||
let loops = std::mem::take(&mut ball.loops);
|
||||
@@ -1211,7 +1245,10 @@ pub fn create_ball_patch(
|
||||
for _ in 0..count {
|
||||
let (Some(c), Some(u), Some(o)) = (curves_3d.next(), uv_curves.next(), orients.next())
|
||||
else {
|
||||
return Err(SolidError::sm_err(OP_PATCH));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_PATCH,
|
||||
"a ball loop lacks a curve, its uv curve or its orientation",
|
||||
));
|
||||
};
|
||||
c3.push(Some(c));
|
||||
uv.push(u);
|
||||
@@ -1256,6 +1293,25 @@ pub fn create_ball_patch(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Three planes through one line meet in no single point: a typed `Failed`, with the
|
||||
/// `SM_ERR` status the bare status had.
|
||||
#[test]
|
||||
fn planes_through_one_line_are_a_typed_failure() {
|
||||
let on = [Point3::ZERO; 3];
|
||||
let normal = [Vec3::X, Vec3::Y, (Vec3::X + Vec3::Y).unitize().unwrap()];
|
||||
let err = planes_meet(&on, &normal).unwrap_err();
|
||||
assert_eq!(
|
||||
err.blend_failure(),
|
||||
Some(&crate::error::blend::BlendFailure::Failed(
|
||||
"the three faces' planes do not meet in one point"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&err),
|
||||
Some(crate::error::SM_ERR)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choose_is_binomial() {
|
||||
assert_eq!(choose(4, 2), 6.0);
|
||||
|
||||
+21
-10
@@ -652,7 +652,12 @@ pub fn classify_chamfer(
|
||||
tol: f64,
|
||||
solvers: &dyn ChamferSolvers,
|
||||
) -> Result<ChamferClass> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"topology or a solver the chamfer corner's classification reads",
|
||||
)
|
||||
};
|
||||
let n = all_eus.len();
|
||||
if !(3..=MAX_CHAMFERS).contains(&n) {
|
||||
return Ok(ChamferClass::Declined(
|
||||
@@ -989,11 +994,11 @@ fn corner_patch(
|
||||
/// `SM_ERR` where the corner is not as its topology made it or a solved crossing is not
|
||||
/// at its exact point; [`SolidError::null_pointer`] without the event.
|
||||
pub fn place_chamfer_vertices(pseudo: &mut FilletBrep, corner: &FilletCorner) -> Result<()> {
|
||||
let err = || SolidError::sm_err(OP_PLACE);
|
||||
let err = || SolidError::blend_failed(OP_PLACE, "a chamfer vertex could not be placed");
|
||||
let event = &corner
|
||||
.chamfer_corner
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_PLACE))?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_PLACE, "the chamfer corner's event"))?
|
||||
.definition;
|
||||
let count = event.crossings.len();
|
||||
if corner.vertices.len() != count + usize::from(event.mitre.is_some()) {
|
||||
@@ -1073,8 +1078,14 @@ pub fn chamfer_corner_edge_geom(
|
||||
pseudo: &mut FilletBrep,
|
||||
corner: &mut FilletCorner,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_EDGES);
|
||||
let err = || SolidError::sm_err(OP_EDGES);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_EDGES,
|
||||
"topology or geometry the chamfer corner's edges are built from",
|
||||
)
|
||||
};
|
||||
let err =
|
||||
|| SolidError::blend_failed(OP_EDGES, "the chamfer corner's edges could not be built");
|
||||
let event = &corner.chamfer_corner.as_ref().ok_or_else(nul)?.definition;
|
||||
let mitre = event.is_mitre();
|
||||
let mut size = 1.0_f64;
|
||||
@@ -1250,9 +1261,9 @@ fn bilinear_face(
|
||||
let mut curves_3d = Vec::new();
|
||||
let mut uv_curves = Vec::new();
|
||||
for (i, &(p, a)) in corners.iter().enumerate() {
|
||||
let (q, b) = *corners
|
||||
.get((i + 1) % 4)
|
||||
.ok_or_else(|| SolidError::sm_err(OP_EDGES))?;
|
||||
let (q, b) = *corners.get((i + 1) % 4).ok_or_else(|| {
|
||||
SolidError::blend_failed(OP_EDGES, "a corner past the bilinear face's four")
|
||||
})?;
|
||||
let curve = Curve::from(Line::create_line_segment(3, p, q, None)?);
|
||||
let ivl = curve.natural_interval();
|
||||
uv_curves.push(uv_segment(a, b, [ivl.min(), ivl.max()])?);
|
||||
@@ -1278,7 +1289,7 @@ fn bilinear_face(
|
||||
/// `SM_ERR` where the order does not cover the crossings; those of the surface and curve
|
||||
/// constructions.
|
||||
fn patch_faces(event: &mut ChamferCorner, patch: &ChamferPatch) -> Result<()> {
|
||||
let err = || SolidError::sm_err(OP_EDGES);
|
||||
let err = || SolidError::blend_failed(OP_EDGES, "the chamfer patch's faces could not be built");
|
||||
let xs = &event.definition.crossings;
|
||||
let n = patch.around.len();
|
||||
if n != xs.len() || n < 4 {
|
||||
@@ -1325,7 +1336,7 @@ pub fn create_chamfer_patch(
|
||||
corner: &mut FilletCorner,
|
||||
env: &mut CornerPatchEnv<'_, '_>,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_PATCH);
|
||||
let nul = || SolidError::blend_missing(OP_PATCH, "the chamfer corner's event or topology");
|
||||
let event = corner.chamfer_corner.as_mut().ok_or_else(nul)?;
|
||||
let faces = std::mem::take(&mut event.faces);
|
||||
if faces.is_empty() {
|
||||
|
||||
+33
-9
@@ -119,7 +119,7 @@ pub struct EndRefusal {
|
||||
}
|
||||
|
||||
fn nul() -> SolidError {
|
||||
SolidError::null_pointer(OP)
|
||||
SolidError::blend_missing(OP, "topology, a solver or geometry the End reads")
|
||||
}
|
||||
|
||||
fn face_of(target: &Brep, eu: EdgeuseId) -> Result<FaceId> {
|
||||
@@ -440,7 +440,10 @@ fn ball_touching_at(
|
||||
}
|
||||
}
|
||||
if f_index > 1 {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"no fillet surface lies on the side face",
|
||||
));
|
||||
}
|
||||
let offset_f = &fs.surface(f_index)?.ok_or_else(nul)?.surface;
|
||||
let f = offset_f.base_surface();
|
||||
@@ -458,7 +461,10 @@ fn ball_touching_at(
|
||||
&DropPointOptions::default(),
|
||||
)?;
|
||||
if !dropped.success {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"the point does not drop onto the fillet's base surface",
|
||||
));
|
||||
}
|
||||
let normal = f.evaluate_normal(k, dropped.result_uv, Side::Left, Side::Left)?;
|
||||
let g_domain = g.natural_uv_domain_with_nurb(k)?;
|
||||
@@ -466,7 +472,10 @@ fn ball_touching_at(
|
||||
let centre = p + normal * (sign * rho);
|
||||
let at = g.drop_point::<NoObject>(k, centre, &g_domain, &DropPointOptions::default())?;
|
||||
if !at.success {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"a centre does not drop onto the other face",
|
||||
));
|
||||
}
|
||||
Ok(at.gap - rho)
|
||||
};
|
||||
@@ -856,12 +865,18 @@ pub fn side_edge_chain_to(
|
||||
}
|
||||
}
|
||||
let Some(prev) = prev else {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"the side edge chain has no continuation",
|
||||
));
|
||||
};
|
||||
back.push(prev);
|
||||
curr = prev;
|
||||
}
|
||||
Err(SolidError::sm_err(OP))
|
||||
Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"the side edge chain does not reach the vertex within its step bound",
|
||||
))
|
||||
}
|
||||
|
||||
/// The fork's comment of a concave round's mixed End whose rail passes over the side
|
||||
@@ -958,7 +973,10 @@ fn refuse_end_past_side_edge(
|
||||
let (past, fit) =
|
||||
radius_reaching_side_edge_end(k, target, corner, solvers, fil_eu, side_eu, other_face)?;
|
||||
if !past {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"the rail does not run past the side edge's end",
|
||||
));
|
||||
}
|
||||
Ok(EndRefusal {
|
||||
comment: describe_end_past_side_edge(fit),
|
||||
@@ -1139,7 +1157,10 @@ fn calc_end_cap_edges(
|
||||
if !pseudo.fillet_vertex(start)?.is_processed()
|
||||
|| !pseudo.fillet_vertex(end)?.is_processed()
|
||||
{
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"a vertex of the cap edge is not processed",
|
||||
));
|
||||
}
|
||||
// The rail's end must lie on the side face (a side face ending tangentially at the
|
||||
// vertex, as a round at its contact line, can have the rail roll over onto the face
|
||||
@@ -1769,7 +1790,10 @@ pub fn calc_end_edge_geom(
|
||||
first
|
||||
};
|
||||
if fil_faces.contains(&end_face) {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"the rail ends on one of the fillet's own faces",
|
||||
));
|
||||
}
|
||||
solvers.solver_of_edgeuse(fil_eu)?;
|
||||
let vertex = corner.vertex.ok_or_else(nul)?;
|
||||
|
||||
+91
-26
@@ -104,12 +104,22 @@ fn of2m<T>(a: &mut [T; 2], i: usize) -> &mut T {
|
||||
|
||||
/// `v[i]`, or `SM_ERR` for `op` past its end.
|
||||
fn nth<'a, T>(v: &'a [T], i: usize, op: &'static str) -> Result<&'a T> {
|
||||
v.get(i).ok_or_else(|| SolidError::sm_err(op))
|
||||
v.get(i).ok_or_else(|| {
|
||||
SolidError::blend_failed(
|
||||
op,
|
||||
"an index past the end of a list the mixed corner builds",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `v[i]` mutably, or `SM_ERR` for `op` past its end.
|
||||
fn nth_mut<'a, T>(v: &'a mut [T], i: usize, op: &'static str) -> Result<&'a mut T> {
|
||||
v.get_mut(i).ok_or_else(|| SolidError::sm_err(op))
|
||||
v.get_mut(i).ok_or_else(|| {
|
||||
SolidError::blend_failed(
|
||||
op,
|
||||
"an index past the end of a list the mixed corner builds",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `SmMixedRing`: the Mixed corner's parts at its vertex, read off the ring: the convex
|
||||
@@ -199,7 +209,12 @@ pub(crate) fn mixed_ring<S: Copy>(
|
||||
all_eus: &[EdgeuseId],
|
||||
solver_of: &dyn Fn(EdgeuseId) -> Option<S>,
|
||||
) -> Result<Option<(MixedRing, [S; 2])>> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"topology or a solver the mixed corner's ring is found from",
|
||||
)
|
||||
};
|
||||
let &[eu0, eu1_] = solver_eus else {
|
||||
return Ok(None);
|
||||
};
|
||||
@@ -232,7 +247,8 @@ pub(crate) fn mixed_ring<S: Copy>(
|
||||
|
||||
/// The face of `eu`'s sector other than `not`, where the sector holds `not`.
|
||||
fn other_face(b: &Brep, eu: EdgeuseId, not: FaceId) -> Result<Option<FaceId>> {
|
||||
let radial = eu1::radial(b, eu)?.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?;
|
||||
let radial = eu1::radial(b, eu)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_CLASSIFY, "the edge use's radial"))?;
|
||||
let faces = [eu1::face(b, eu)?, eu1::face(b, radial)?];
|
||||
Ok(match faces {
|
||||
[Some(f), Some(g)] if f == not => Some(g),
|
||||
@@ -342,7 +358,12 @@ pub fn classify_mixed<'s>(
|
||||
let Some((ring, [fs0, fs1])) = mixed_ring(k, b, solver_eus, all_eus, solver_of)? else {
|
||||
return Ok(false);
|
||||
};
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"topology or a solver the mixed corner's classification reads",
|
||||
)
|
||||
};
|
||||
let convex_e = eu1::edge(b, ring.convex)?.ok_or_else(nul)?;
|
||||
let concave_e = eu1::edge(b, ring.concave)?.ok_or_else(nul)?;
|
||||
if !test_g1_continuity(
|
||||
@@ -476,9 +497,13 @@ fn leaving(k: &Kernel, b: &Brep, e: EdgeId, vertex: VertexId, op: &'static str)
|
||||
let at_start = edge::start_vertex(b, e)? == Some(vertex);
|
||||
if !at_start && edge::end_vertex(b, e)? != Some(vertex) {
|
||||
// the edge does not meet the vertex
|
||||
return Err(SolidError::sm_err(op));
|
||||
return Err(SolidError::blend_failed(
|
||||
op,
|
||||
"the edge does not meet the vertex",
|
||||
));
|
||||
}
|
||||
let curve = edge::curve(b, e)?.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
let curve =
|
||||
edge::curve(b, e)?.ok_or_else(|| SolidError::blend_missing(op, "the edge's curve"))?;
|
||||
let ivl = edge::interval(b, e)?;
|
||||
let pv = curve.evaluate(
|
||||
k,
|
||||
@@ -490,7 +515,12 @@ fn leaving(k: &Kernel, b: &Brep, e: EdgeId, vertex: VertexId, op: &'static str)
|
||||
let mut d = pv
|
||||
.get(1)
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::sm_err(op))?
|
||||
.ok_or_else(|| {
|
||||
SolidError::blend_failed(
|
||||
op,
|
||||
"the curve's first derivative at the vertex is not there",
|
||||
)
|
||||
})?
|
||||
.unitize()?;
|
||||
if !at_start {
|
||||
d = -d;
|
||||
@@ -514,7 +544,9 @@ fn param_of(k: &Kernel, curve: &Curve, pnt: Point3, tol: f64, op: &'static str)
|
||||
..Default::default()
|
||||
},
|
||||
)?;
|
||||
let first = sols.get(0).ok_or_else(|| SolidError::sm_err(op))?;
|
||||
let first = sols.get(0).ok_or_else(|| {
|
||||
SolidError::blend_failed(op, "the point has no nearest point on the curve")
|
||||
})?;
|
||||
first.start.param(0)
|
||||
}
|
||||
|
||||
@@ -540,7 +572,9 @@ fn curve_surface(
|
||||
let mut best = BIG_DOUBLE;
|
||||
let mut out = None;
|
||||
for i in 0..sols.len() {
|
||||
let sol = sols.get(i).ok_or_else(|| SolidError::sm_err(op))?;
|
||||
let sol = sols
|
||||
.get(i)
|
||||
.ok_or_else(|| SolidError::blend_failed(op, "an intersection past the list found"))?;
|
||||
let pnt = curve.evaluate_point(k, sol.start.param(0)?)?;
|
||||
if pnt.distance(near) < best {
|
||||
best = pnt.distance(near);
|
||||
@@ -548,24 +582,27 @@ fn curve_surface(
|
||||
}
|
||||
}
|
||||
// Mixed corner: a rail does not meet the other blend's rail surface
|
||||
out.ok_or_else(|| SolidError::sm_err(op))
|
||||
out.ok_or_else(|| {
|
||||
SolidError::blend_failed(op, "the rail does not meet the other blend's rail surface")
|
||||
})
|
||||
}
|
||||
|
||||
/// `sm_MixedRailWall`: the surface swept by a rail on the plane T along T's normal,
|
||||
/// reaching `reach` either side; the rail first continued past both ends as itself (its
|
||||
/// end spans' own polynomials) by a quarter of `reach`.
|
||||
fn rail_wall(rail: &Curve, normal: Vec3, reach: f64, op: &'static str) -> Result<Surface> {
|
||||
let b0 = bspline_curve(rail).ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
let b0 = bspline_curve(rail)
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the rail's B-spline curve"))?;
|
||||
let b1 = b0
|
||||
.create_extended_curve_by_distance(
|
||||
0.25 * reach,
|
||||
CurveEnd::Start,
|
||||
ContinuityType::CInfinity,
|
||||
)?
|
||||
.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the rail extended at its start"))?;
|
||||
let mut bb = b1
|
||||
.create_extended_curve_by_distance(0.25 * reach, CurveEnd::End, ContinuityType::CInfinity)?
|
||||
.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the rail extended at its end"))?;
|
||||
let shift = normal * (-reach);
|
||||
bb.transform(
|
||||
&Axis2Placement::new(
|
||||
@@ -593,7 +630,7 @@ fn rails(
|
||||
solvers: [&FilletSolver; 2],
|
||||
op: &'static str,
|
||||
) -> Result<Rails> {
|
||||
let nul = || SolidError::null_pointer(op);
|
||||
let nul = || SolidError::blend_missing(op, "a solver's rail or surface");
|
||||
let walls = [mixed.ring.f1, mixed.ring.f2];
|
||||
let mut on_t = Vec::new();
|
||||
let mut on_wall = Vec::new();
|
||||
@@ -623,10 +660,10 @@ fn plane_of_t(k: &Kernel, target: &Brep, t: FaceId, tol: f64, op: &'static str)
|
||||
.face(t)?
|
||||
.surface
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the face's surface"))?;
|
||||
let planar = s.is_planar(k, tol)?;
|
||||
if !planar.is_planar {
|
||||
return Err(SolidError::sm_err(op));
|
||||
return Err(SolidError::blend_failed(op, "the face is not planar"));
|
||||
}
|
||||
planar.normal.unitize()
|
||||
}
|
||||
@@ -650,7 +687,8 @@ pub fn calc_mixed_vert_geom(
|
||||
corner: &FilletCorner,
|
||||
solvers: &dyn SweptSolvers,
|
||||
) -> Result<()> {
|
||||
let err = || SolidError::sm_err(OP_VERT);
|
||||
let err =
|
||||
|| SolidError::blend_failed(OP_VERT, "the mixed corner's vertices could not be placed");
|
||||
let mixed = corner.mixed.as_ref().ok_or_else(err)?;
|
||||
let &[x, a, p, v, q, bv] = corner.vertices.as_slice() else {
|
||||
return Err(err());
|
||||
@@ -690,11 +728,11 @@ pub fn calc_mixed_vert_geom(
|
||||
}
|
||||
|
||||
// P: B1's wall rail crosses the seam; Q: B2's wall rail crosses the seam's line past V
|
||||
let seam_e =
|
||||
eu1::edge(target, mixed.ring.seam)?.ok_or_else(|| SolidError::null_pointer(OP_VERT))?;
|
||||
let seam_e = eu1::edge(target, mixed.ring.seam)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_VERT, "the seam's edge"))?;
|
||||
let vertex = corner
|
||||
.vertex
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_VERT))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_VERT, "the corner's vertex"))?;
|
||||
let along = leaving(k, target, seam_e, vertex, OP_VERT)?;
|
||||
let line = Curve::from(BSplineCurve::create_line_segment(
|
||||
3,
|
||||
@@ -775,7 +813,7 @@ fn normal_at(
|
||||
.face(face)?
|
||||
.surface
|
||||
.clone()
|
||||
.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the face's surface"))?;
|
||||
let dom = surface.natural_uv_domain_with_nurb(k)?;
|
||||
let mut test_on: Option<bool> = None;
|
||||
let mut classify = |k: &Kernel, uv: Point2, src_tol: f64| -> Result<bool> {
|
||||
@@ -799,7 +837,9 @@ fn normal_at(
|
||||
SolutionRequested::Single,
|
||||
Some(&mut classify),
|
||||
)?;
|
||||
let sol = sols.get(0).ok_or_else(|| SolidError::sm_err(op))?;
|
||||
let sol = sols.get(0).ok_or_else(|| {
|
||||
SolidError::blend_failed(op, "the point has no nearest point on the face")
|
||||
})?;
|
||||
surface.evaluate_normal(
|
||||
k,
|
||||
Point2::new(sol.start.param(0)?, sol.start.param(1)?),
|
||||
@@ -833,8 +873,13 @@ pub fn calc_mixed_edge_geom(
|
||||
corner: &mut FilletCorner,
|
||||
solvers: &dyn SweptSolvers,
|
||||
) -> Result<()> {
|
||||
let err = || SolidError::sm_err(OP_EDGE);
|
||||
let nul = || SolidError::null_pointer(OP_EDGE);
|
||||
let err = || SolidError::blend_failed(OP_EDGE, "the mixed corner's edges could not be built");
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_EDGE,
|
||||
"topology or geometry the mixed corner's edges are built from",
|
||||
)
|
||||
};
|
||||
let mixed = corner.mixed.clone().ok_or_else(err)?;
|
||||
let ring = mixed.ring;
|
||||
for &v in &corner.vertices {
|
||||
@@ -1512,7 +1557,7 @@ pub fn create_mixed_patches(
|
||||
corner: &mut FilletCorner,
|
||||
env: &mut CornerPatchEnv<'_, '_>,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_PATCH);
|
||||
let nul = || SolidError::blend_missing(OP_PATCH, "the mixed corner's event or topology");
|
||||
for f in 0..2 {
|
||||
let mixed = corner.mixed.as_mut().ok_or_else(nul)?;
|
||||
let surface = of2m(&mut mixed.surfaces, f).take().ok_or_else(nul)?;
|
||||
@@ -1603,4 +1648,24 @@ mod tests {
|
||||
// an edge that does not meet the vertex is an error
|
||||
assert!(leaving(&k, &b, e, off, "test").is_err());
|
||||
}
|
||||
|
||||
/// An edge that does not meet the vertex is a typed `Failed` of the caller's operation,
|
||||
/// with the `SM_ERR` status the bare status had.
|
||||
#[test]
|
||||
fn an_edge_off_the_vertex_is_a_typed_failure() {
|
||||
let k = Kernel::default();
|
||||
let (b, e, _, _, off) = kinked_edge();
|
||||
let err = leaving(&k, &b, e, off, OP_VERT).unwrap_err();
|
||||
assert_eq!(
|
||||
err.blend_failure(),
|
||||
Some(&crate::error::blend::BlendFailure::Failed(
|
||||
"the edge does not meet the vertex"
|
||||
))
|
||||
);
|
||||
assert_eq!(err.operation(), OP_VERT);
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&err),
|
||||
Some(crate::error::SM_ERR)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ impl Srf {
|
||||
}
|
||||
|
||||
fn err() -> SolidError {
|
||||
SolidError::sm_err(OP)
|
||||
SolidError::blend_failed(OP, "an index past the mixed corner's polynomial tables")
|
||||
}
|
||||
|
||||
fn g<T>(v: &[T], i: usize) -> Result<&T> {
|
||||
|
||||
+124
-27
@@ -350,7 +350,7 @@ pub fn overlapping_setbacks(
|
||||
// the largest setback certified to fit: from the length's lower side (#3184)
|
||||
for e in vertex::edges(b, vtx, None)? {
|
||||
let curve = edge::evaluation_curve(b, e)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_TOPOLOGY))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_TOPOLOGY, "an edge's curve"))?;
|
||||
// a bracket as wide as the corner's tolerance, as the Setback's room asks it
|
||||
// (`set_backs`)
|
||||
let length = curve.length_bound(k, &edge::interval(b, e)?, corner.approx_tol_3d)?;
|
||||
@@ -455,7 +455,7 @@ fn smax(a: f64, b: f64) -> f64 {
|
||||
/// `sm_SbAwayFrom`: the unit direction of `e` away from `vertex`, from its curve's end
|
||||
/// tangent there.
|
||||
fn away_from(k: &Kernel, b: &Brep, e: EdgeId, vertex: VertexId) -> Result<Vec3> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || SolidError::blend_missing(OP_CLASSIFY, "the edge or its curve");
|
||||
let curve = edge::curve(b, e)?.ok_or_else(nul)?;
|
||||
let ivl = edge::interval(b, e)?;
|
||||
let at_start = edge::start_vertex(b, e)? == Some(vertex);
|
||||
@@ -493,7 +493,8 @@ fn corner_planes(
|
||||
tol: f64,
|
||||
convex: bool,
|
||||
) -> Result<Option<CornerPlanes>> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul =
|
||||
|| SolidError::blend_missing(OP_CLASSIFY, "topology the corner's planes are found from");
|
||||
let n = all_eus.len();
|
||||
let mut away = Vec::with_capacity(n);
|
||||
for &eu in all_eus {
|
||||
@@ -566,7 +567,7 @@ fn quadric_face(
|
||||
vpoint: Point3,
|
||||
tol: f64,
|
||||
) -> Result<Option<Face>> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || SolidError::blend_missing(OP_CLASSIFY, "the face's surface");
|
||||
let surface = face2::surface(b, face)?.ok_or_else(nul)?;
|
||||
let q = BlendQuadric::from_surface(k, surface, face2::uv_domain(b, face)?.mid(), tol)?;
|
||||
let zero = Vec3::new(0.0, 0.0, 0.0);
|
||||
@@ -616,7 +617,8 @@ fn corner_quadrics(
|
||||
tol: f64,
|
||||
convex: bool,
|
||||
) -> Result<Option<CornerPlanes>> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul =
|
||||
|| SolidError::blend_missing(OP_CLASSIFY, "topology the corner's quadrics are found from");
|
||||
let n = all_eus.len();
|
||||
let mut away = Vec::with_capacity(n);
|
||||
for &eu in all_eus {
|
||||
@@ -742,7 +744,7 @@ fn along_edge(
|
||||
c: Point3,
|
||||
tol: f64,
|
||||
) -> Result<Option<f64>> {
|
||||
let nul = || SolidError::null_pointer(OP_TOPOLOGY);
|
||||
let nul = || SolidError::blend_missing(OP_TOPOLOGY, "the edge or its curve");
|
||||
// the curve as its edge's (its length's cache at the edge's tolerance, as the fork's
|
||||
// pEdge->GetCurve() reads it through its owner)
|
||||
let curve = edge::evaluation_curve(b, e)?.ok_or_else(nul)?;
|
||||
@@ -804,7 +806,12 @@ fn curved_least(
|
||||
convex: bool,
|
||||
tol: f64,
|
||||
) -> Result<Option<Vec<f64>>> {
|
||||
let nul = || SolidError::null_pointer(OP_TOPOLOGY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_TOPOLOGY,
|
||||
"topology or geometry the least curved setbacks are found from",
|
||||
)
|
||||
};
|
||||
let n = all_eus.len();
|
||||
let mut least = vec![0.0; n];
|
||||
let s = if convex { 1.0 } else { -1.0 };
|
||||
@@ -961,7 +968,12 @@ fn set_backs(
|
||||
curved: bool,
|
||||
convex: bool,
|
||||
) -> Result<core::result::Result<SetBacks, Option<&'static str>>> {
|
||||
let nul = || SolidError::null_pointer(OP_TOPOLOGY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_TOPOLOGY,
|
||||
"topology or geometry the setbacks are found from",
|
||||
)
|
||||
};
|
||||
let n = all_eus.len();
|
||||
let (mut setbacks, least) = if curved {
|
||||
let radii: Vec<f64> = edges.iter().map(|e| e.radius).collect();
|
||||
@@ -1093,7 +1105,12 @@ pub fn classify_setback(
|
||||
fills: &[SetBackFill],
|
||||
marked: bool,
|
||||
) -> Result<SetbackClass> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"topology or a solver the setback corner's classification reads",
|
||||
)
|
||||
};
|
||||
let none = Ok(SetbackClass {
|
||||
event: BlendCornerEvent::None,
|
||||
order: 1,
|
||||
@@ -1284,8 +1301,18 @@ pub fn make_setback_topology(
|
||||
solvers,
|
||||
fills,
|
||||
} = env;
|
||||
let nul = || SolidError::null_pointer(OP_TOPOLOGY);
|
||||
let err = || SolidError::sm_err(OP_TOPOLOGY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_TOPOLOGY,
|
||||
"topology or a solver the setback corner's topology is made from",
|
||||
)
|
||||
};
|
||||
let err = || {
|
||||
SolidError::blend_failed(
|
||||
OP_TOPOLOGY,
|
||||
"the setback corner's topology could not be made",
|
||||
)
|
||||
};
|
||||
let all = corner.all_eus.clone().ok_or_else(nul)?;
|
||||
let solver_eus = corner.solver_eus.clone().ok_or_else(nul)?;
|
||||
let n = all.len();
|
||||
@@ -1605,8 +1632,13 @@ fn gather_ends(
|
||||
faces: &[FaceId],
|
||||
tol: f64,
|
||||
) -> Result<Gathered> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let err = || SolidError::sm_err(OP_FILL);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"topology or a solver the fill's ends are gathered from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_FILL, "the fill's ends could not be gathered");
|
||||
let vtx = corner.vertex.ok_or_else(nul)?;
|
||||
let n = faces.len();
|
||||
let mut surfaces: Vec<Surface> = Vec::with_capacity(n);
|
||||
@@ -1823,7 +1855,12 @@ impl Growth<'_> {
|
||||
solvers: &mut S,
|
||||
scale: f64,
|
||||
) -> Result<Option<Ends>> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"topology or a solver the fill's ends are placed from",
|
||||
)
|
||||
};
|
||||
let sb = corner.setback.as_mut().ok_or_else(nul)?;
|
||||
let mut dist: f64 = 0.0;
|
||||
for ((s, &base), &most) in sb.setbacks.iter_mut().zip(&self.base).zip(&self.most) {
|
||||
@@ -1908,7 +1945,7 @@ fn grow_curved<S: SetbackSolvers>(
|
||||
solvers: &mut S,
|
||||
asked: Option<SetBackFill>,
|
||||
) -> Result<core::result::Result<Ends, SetbackRefusal>> {
|
||||
let err = || SolidError::sm_err(OP_FILL);
|
||||
let err = || SolidError::blend_failed(OP_FILL, "the curved fill could not grow");
|
||||
if growth.trial(pseudo, corner, solvers, 2.0)?.is_none() {
|
||||
let why = if let Some(a) = asked {
|
||||
format!(
|
||||
@@ -2065,7 +2102,7 @@ fn rail_toward_vertex(
|
||||
r: f64,
|
||||
sign: f64,
|
||||
) -> Result<Option<Vec3>> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let nul = || SolidError::blend_missing(OP_FILL, "the edge or its curve");
|
||||
let vtx = corner.vertex.ok_or_else(nul)?;
|
||||
let Some(t) = rail_direction(face, p, other, q, r, sign) else {
|
||||
return Ok(None);
|
||||
@@ -2094,7 +2131,12 @@ fn corner_springs(
|
||||
corner: &FilletCorner,
|
||||
planes: &CornerPlanes,
|
||||
) -> Result<core::result::Result<Vec<SpringCurve>, Springs>> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"topology or a solver the corner's spring curves are built from",
|
||||
)
|
||||
};
|
||||
let all = corner.all_eus.as_ref().ok_or_else(nul)?;
|
||||
let sign = if corner.setback.as_ref().ok_or_else(nul)?.convex {
|
||||
1.0
|
||||
@@ -2159,7 +2201,12 @@ fn springs_on_faces(
|
||||
planes: &CornerPlanes,
|
||||
tol: f64,
|
||||
) -> Result<Springs> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"topology or a solver the spring curves on the faces are built from",
|
||||
)
|
||||
};
|
||||
let vtx = corner.vertex.ok_or_else(nul)?;
|
||||
let springs = match corner_springs(k, target, pseudo, corner, planes)? {
|
||||
Ok(springs) => springs,
|
||||
@@ -2231,7 +2278,7 @@ fn springs_refusal<S: SetbackSolvers>(
|
||||
fills: &[SetBackFill],
|
||||
tol: f64,
|
||||
) -> Result<Option<SetbackRefusal>> {
|
||||
let err = || SolidError::sm_err(OP_FILL);
|
||||
let err = || SolidError::blend_failed(OP_FILL, "the spring curves could not be checked");
|
||||
let at_base = springs_on_faces(k, target, pseudo, corner, planes, tol)?;
|
||||
if at_base != Springs::Leave {
|
||||
return Ok(None);
|
||||
@@ -2375,8 +2422,13 @@ pub fn build_setback_fill<S: SetbackSolvers>(
|
||||
solvers: &mut S,
|
||||
fills: &[SetBackFill],
|
||||
) -> Result<core::result::Result<(), SetbackRefusal>> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let err = || SolidError::sm_err(OP_FILL);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"topology or a solver the setback fill is built from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_FILL, "the setback fill could not be built");
|
||||
let all = corner.all_eus.clone().ok_or_else(nul)?;
|
||||
let sb = corner.setback.as_ref().ok_or_else(err)?;
|
||||
if sb.overflows {
|
||||
@@ -2628,8 +2680,13 @@ fn open_setback_springs(
|
||||
planes: &[Face],
|
||||
edges: &[Edge],
|
||||
) -> Result<core::result::Result<(), SetbackRefusal>> {
|
||||
let nul = || SolidError::null_pointer(OP_FILL);
|
||||
let err = || SolidError::sm_err(OP_FILL);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"topology or a solver the open setback's springs are built from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_FILL, "the open setback's springs could not be built");
|
||||
let n = corner.all_eus.as_ref().ok_or_else(nul)?.len();
|
||||
if corner.vertices.len() != 3 * n
|
||||
|| corner.edges.len() != 3 * n
|
||||
@@ -2733,7 +2790,10 @@ fn one_weight_polynomial(curve: &mut BSplineCurve) -> Result<()> {
|
||||
}
|
||||
let points = curve.control_points()?;
|
||||
let Some(w0) = points.first().map(|c| c.w) else {
|
||||
return Err(SolidError::null_pointer(OP_FILL));
|
||||
return Err(SolidError::blend_missing(
|
||||
OP_FILL,
|
||||
"the curve's first control point",
|
||||
));
|
||||
};
|
||||
if points.iter().skip(1).any(|c| c.w != w0) {
|
||||
return Ok(());
|
||||
@@ -2765,7 +2825,10 @@ fn keep_definition(
|
||||
) -> Result<Arc<SetbackFill>> {
|
||||
let definition = SetbackFill::create(k, faces, rounds, order, tol, convex, worst)?;
|
||||
if !definition.is_fill(&definition.refill(k)?, &fill.patches) {
|
||||
return Err(SolidError::sm_err(OP_FILL));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_FILL,
|
||||
"the fill's definition does not rebuild its patches",
|
||||
));
|
||||
}
|
||||
Ok(Arc::new(definition))
|
||||
}
|
||||
@@ -2785,7 +2848,8 @@ pub fn create_setback_patches(
|
||||
corner: &mut FilletCorner,
|
||||
env: &mut CornerPatchEnv<'_, '_>,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_PATCHES);
|
||||
let nul =
|
||||
|| SolidError::blend_missing(OP_PATCHES, "the setback corner's event, fill or topology");
|
||||
if corner.setback.as_ref().is_some_and(|sb| sb.open) {
|
||||
return Ok(()); // an open G1 corner: its hole is left (OpenSetBackSprings)
|
||||
}
|
||||
@@ -2796,7 +2860,10 @@ pub fn create_setback_patches(
|
||||
.and_then(|sb| sb.fill.take())
|
||||
.ok_or_else(nul)?;
|
||||
if fill.patches.len() != 2 * n {
|
||||
return Err(SolidError::sm_err(OP_PATCHES));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_PATCHES,
|
||||
"the fill does not hold two patches per round",
|
||||
));
|
||||
}
|
||||
let definition = corner.setback.as_ref().and_then(|sb| sb.definition.clone());
|
||||
let fillet = env.fillet.as_mut().ok_or_else(nul)?;
|
||||
@@ -2890,6 +2957,36 @@ pub fn create_setback_patches(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A box with its first edge's curve taken away, the edge and its start vertex.
|
||||
fn box_with_curveless_edge() -> (Brep, EdgeId, VertexId) {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = crate::primitive::api_primitives::create_box(&mut k, Vec3::ZERO, 1.0, 1.0, 1.0)
|
||||
.unwrap();
|
||||
let e = b.edges().ids().next().unwrap();
|
||||
let v = edge::start_vertex(&b, e).unwrap().unwrap();
|
||||
b.edge_mut(e).unwrap().curve = None;
|
||||
(b, e, v)
|
||||
}
|
||||
|
||||
/// An edge without a curve has no direction away from its vertex: a typed `Missing`
|
||||
/// failure, with the null-pointer status the bare status had.
|
||||
#[test]
|
||||
fn an_edge_without_a_curve_is_a_typed_missing_failure() {
|
||||
let (b, e, v) = box_with_curveless_edge();
|
||||
let err = away_from(&Kernel::default(), &b, e, v).unwrap_err();
|
||||
assert_eq!(
|
||||
err.blend_failure(),
|
||||
Some(&crate::error::blend::BlendFailure::Missing(
|
||||
"the edge or its curve"
|
||||
))
|
||||
);
|
||||
assert_eq!(err.operation(), OP_CLASSIFY);
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&err),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER)
|
||||
);
|
||||
}
|
||||
use crate::blend::blend_setback::FaceKind;
|
||||
|
||||
/// The rail of a ball of radius `r` rolling between `face` and the plane through `q` with
|
||||
|
||||
+46
-7
@@ -184,7 +184,7 @@ pub(crate) fn fmt_g(v: f64) -> String {
|
||||
|
||||
/// `sm_TangentAtVertex`: the unit tangent of `e`'s curve at its end on `vertex`.
|
||||
fn tangent_at_vertex(k: &Kernel, b: &Brep, e: EdgeId, vertex: VertexId) -> Result<Vec3> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || SolidError::blend_missing(OP_CLASSIFY, "the edge or its curve");
|
||||
let curve = edge::curve(b, e)?.ok_or_else(nul)?;
|
||||
let ivl = edge::interval(b, e)?;
|
||||
let t = if edge::start_vertex(b, e)? == Some(vertex) {
|
||||
@@ -207,7 +207,7 @@ fn meridian_angle_deg(w: Vec3, x: Vec3, z: Vec3) -> f64 {
|
||||
fn offset_radius(solver: &FilletSolver, rail: usize, op: &'static str) -> Result<f64> {
|
||||
let slot = solver
|
||||
.surface(rail)?
|
||||
.ok_or_else(|| SolidError::null_pointer(op))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(op, "the rail's offset surface"))?;
|
||||
Ok(slot.surface.offset_distance().abs())
|
||||
}
|
||||
|
||||
@@ -235,7 +235,12 @@ pub fn classify_swept(
|
||||
tangency_tol_deg: f64,
|
||||
solvers: &dyn SweptSolvers,
|
||||
) -> Result<Option<TurnRefusal>> {
|
||||
let nul = || SolidError::null_pointer(OP_CLASSIFY);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_CLASSIFY,
|
||||
"topology or a solver the swept turn's classification reads",
|
||||
)
|
||||
};
|
||||
let refuse = |error: FilletErrorType, why: &str| {
|
||||
Ok(Some(TurnRefusal {
|
||||
error,
|
||||
@@ -369,8 +374,13 @@ pub fn swept_corner_edge_geom(
|
||||
corner: &mut FilletCorner,
|
||||
solvers: &dyn SweptSolvers,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_BUILD);
|
||||
let err = || SolidError::sm_err(OP_BUILD);
|
||||
let nul = || {
|
||||
SolidError::blend_missing(
|
||||
OP_BUILD,
|
||||
"topology or geometry the swept turn's edges are built from",
|
||||
)
|
||||
};
|
||||
let err = || SolidError::blend_failed(OP_BUILD, "the swept turn's edges could not be built");
|
||||
let swept = corner.swept.as_mut().ok_or_else(nul)?;
|
||||
swept.delete_surface();
|
||||
let side_eu = swept.side_eu;
|
||||
@@ -579,7 +589,7 @@ fn keep_definition(
|
||||
radius: f64,
|
||||
tol: f64,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_BUILD);
|
||||
let nul = || SolidError::blend_missing(OP_BUILD, "the swept turn's event or its patch");
|
||||
let swept = corner.swept.as_ref().ok_or_else(nul)?;
|
||||
let eus = corner.solver_eus.as_deref().ok_or_else(nul)?;
|
||||
let (Some(&eu1_), Some(&eu2_)) = (eus.first(), eus.get(1)) else {
|
||||
@@ -607,7 +617,7 @@ pub fn create_swept_patch(
|
||||
corner: &mut FilletCorner,
|
||||
env: &mut CornerPatchEnv<'_, '_>,
|
||||
) -> Result<()> {
|
||||
let nul = || SolidError::null_pointer(OP_PATCH);
|
||||
let nul = || SolidError::blend_missing(OP_PATCH, "the swept turn's event or topology");
|
||||
let swept = corner.swept.as_mut().ok_or_else(nul)?;
|
||||
let surface = swept.surface.take().ok_or_else(nul)?;
|
||||
// the face takes the surface and the curves
|
||||
@@ -649,6 +659,35 @@ pub fn create_swept_patch(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A box with its first edge's curve taken away, the edge and its start vertex.
|
||||
fn box_with_curveless_edge() -> (Brep, EdgeId, VertexId) {
|
||||
let mut k = Kernel::default();
|
||||
let mut b = crate::primitive::api_primitives::create_box(&mut k, Vec3::ZERO, 1.0, 1.0, 1.0)
|
||||
.unwrap();
|
||||
let e = b.edges().ids().next().unwrap();
|
||||
let v = edge::start_vertex(&b, e).unwrap().unwrap();
|
||||
b.edge_mut(e).unwrap().curve = None;
|
||||
(b, e, v)
|
||||
}
|
||||
|
||||
/// An edge without a curve has no tangent at its vertex: a typed `Missing` failure, with
|
||||
/// the null-pointer status the bare status had.
|
||||
#[test]
|
||||
fn an_edge_without_a_curve_is_a_typed_missing_failure() {
|
||||
let (b, e, v) = box_with_curveless_edge();
|
||||
let err = tangent_at_vertex(&Kernel::default(), &b, e, v).unwrap_err();
|
||||
assert_eq!(
|
||||
err.blend_failure(),
|
||||
Some(&crate::error::blend::BlendFailure::Missing(
|
||||
"the edge or its curve"
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&err),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fmt_g_is_printf_g() {
|
||||
assert_eq!(fmt_g(1.0), "1");
|
||||
|
||||
+38
-5
@@ -138,7 +138,7 @@ const OP: &str = "blend::blend_setback";
|
||||
|
||||
/// `SM_ERR`.
|
||||
fn sm_err() -> SolidError {
|
||||
SolidError::sm_err(OP)
|
||||
SolidError::blend_failed(OP, "the setback corner's construction could not continue")
|
||||
}
|
||||
|
||||
// ---- the ease of a round into another section before its setback ----------------------
|
||||
@@ -348,7 +348,7 @@ pub fn ease_surface_of(
|
||||
) -> Result<BSplineSurface> {
|
||||
let part = |s: &Surface| -> Result<BSplineSurface> {
|
||||
Ok(crate::topology::brep::p3::with_nurb(k, s)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP, "a round surface's NURB form"))?
|
||||
.into_owned())
|
||||
};
|
||||
let blend = |s: &Surface| matches!(s.kind(), SurfaceKind::ProceduralBlend(_));
|
||||
@@ -416,7 +416,7 @@ impl EaseSide<'_> {
|
||||
Self::Net(b) => Ok(*b),
|
||||
Self::Blend(_, s) => match s.into_kind() {
|
||||
SurfaceKind::ProceduralBlend(pb) => Ok(pb.net),
|
||||
_ => Err(SolidError::null_pointer(OP)),
|
||||
_ => Err(SolidError::blend_missing(OP, "the procedural blend's net")),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -792,7 +792,7 @@ impl Refusal {
|
||||
/// `SM_SB_FAIL(.., SM_ERR, reason)`.
|
||||
fn sm_err(reason: &'static str) -> Self {
|
||||
Self {
|
||||
error: sm_err(),
|
||||
error: SolidError::blend_failed(OP, reason),
|
||||
reason: Some(reason),
|
||||
}
|
||||
}
|
||||
@@ -1259,7 +1259,12 @@ fn round_jets(
|
||||
// Copy, then SM_CAST_PTR(SmBSplineSurface, pCopy) (NER): an analytic round's NURB, a
|
||||
// procedural blend's net
|
||||
let mut s = crate::topology::brep::p3::with_nurb(kernel, round.surface)?
|
||||
.ok_or_else(|| Refusal::from(SolidError::null_pointer(OP)))?
|
||||
.ok_or_else(|| {
|
||||
Refusal::from(SolidError::blend_missing(
|
||||
OP,
|
||||
"the round surface's NURB form",
|
||||
))
|
||||
})?
|
||||
.into_owned();
|
||||
let p = s.degree(along);
|
||||
let dom = s.natural_uv_domain();
|
||||
@@ -3139,6 +3144,34 @@ mod tests {
|
||||
assert!(ease_law_on_span(&ramps, 2.5, 3.0).is_err());
|
||||
}
|
||||
|
||||
/// Two ramps over one span are a typed `Failed`, with the `SM_ERR` status the bare
|
||||
/// status had; a refusal's error carries its reason.
|
||||
#[test]
|
||||
fn two_ramps_over_one_span_are_a_typed_failure() {
|
||||
let ramps = [
|
||||
EaseRamp { from: 0.0, to: 4.0 },
|
||||
EaseRamp { from: 1.0, to: 5.0 },
|
||||
];
|
||||
let err = ease_law_on_span(&ramps, 2.0, 3.0).unwrap_err();
|
||||
assert!(matches!(
|
||||
err.blend_failure(),
|
||||
Some(crate::error::blend::BlendFailure::Failed(_))
|
||||
));
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&err),
|
||||
Some(crate::error::SM_ERR)
|
||||
);
|
||||
let refusal = Refusal::sm_err("why");
|
||||
assert_eq!(
|
||||
refusal.error.blend_failure(),
|
||||
Some(&crate::error::blend::BlendFailure::Failed("why"))
|
||||
);
|
||||
assert_eq!(
|
||||
crate::error::upstream_status(&refusal.error),
|
||||
Some(crate::error::SM_ERR)
|
||||
);
|
||||
}
|
||||
|
||||
/// The round of radius r along axis a of the box corner `x, y, z >= 0` over `[s, s + len]`,
|
||||
/// u along it, v across from its rail on face `(a + 1) % 3` to its rail on face
|
||||
/// `(a + 2) % 3`. Order 1: the ball's circle, the rational quadratic arc (`along` 1: linear
|
||||
|
||||
+79
-21
@@ -74,8 +74,9 @@ const OP: &str = "blend::fillet_exact::rational";
|
||||
/// The most spine segments a conic is cut into before the weights are made positive.
|
||||
pub const MAX_SPINE_SEGMENTS: f64 = 1.0e5;
|
||||
|
||||
fn sm_err() -> SolidError {
|
||||
SolidError::blend_failed(OP, "the rational fillet section could not be built")
|
||||
/// `SM_ERR`: the rational fillet section could not be built, as `what` says.
|
||||
fn sm_err(what: &'static str) -> SolidError {
|
||||
SolidError::blend_failed(OP, what)
|
||||
}
|
||||
|
||||
/// `sm_Poly`: a polynomial by its Bernstein coefficients on `[0, 1]`.
|
||||
@@ -471,7 +472,7 @@ fn plane_section(point: Point3, normal: Vec3, q: &BlendQuadric) -> Result<Conic>
|
||||
let dscale = max2(l0.abs(), l1.abs());
|
||||
if dscale <= 0.0 {
|
||||
// the plane meets the quadric in no conic
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the plane meets the quadric in no conic"));
|
||||
}
|
||||
let pick = |a: [f64; 2], k: usize| if k == 0 { a[0] } else { a[1] };
|
||||
let pick_v = |a: [Vec3; 2], k: usize| if k == 0 { a[0] } else { a[1] };
|
||||
@@ -482,7 +483,7 @@ fn plane_section(point: Point3, normal: Vec3, q: &BlendQuadric) -> Result<Conic>
|
||||
let o = 1 - kk;
|
||||
if pick(beta, kk).abs() <= EFF_ZERO * dscale {
|
||||
// the plane meets the quadric in lines
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the plane meets the quadric in lines"));
|
||||
}
|
||||
// s = -(L t^2 + 2 beta_o t + c2) / (2 beta_k): about t0 = -beta_o / L
|
||||
let (lo, bo, bk) = (pick(adl, o), pick(beta, o), pick(beta, kk));
|
||||
@@ -518,7 +519,7 @@ fn plane_section(point: Point3, normal: Vec3, q: &BlendQuadric) -> Result<Conic>
|
||||
)
|
||||
} else {
|
||||
// the plane misses the quadric
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the plane misses the quadric"));
|
||||
};
|
||||
Ok(Conic {
|
||||
kind,
|
||||
@@ -718,7 +719,9 @@ fn nodal_quartic(
|
||||
let g11 = g1x * g1x + g1y * g1y + g1z * g1z;
|
||||
if g11 <= 0.0 {
|
||||
// no normal at the node
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the quadrics' intersection has no normal at the node",
|
||||
));
|
||||
}
|
||||
let kk = (g1x * g2x + g1y * g2y + g1z * g2z) / g11;
|
||||
let mut c = [[0.0; 3]; 3];
|
||||
@@ -746,7 +749,7 @@ fn nodal_quartic(
|
||||
let d = c12 * c12 - c11 * c22;
|
||||
if d < 0.0 || (c11 == 0.0 && c22 == 0.0) {
|
||||
// the node is isolated
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the node of the quadrics' intersection is isolated"));
|
||||
}
|
||||
let mut branch = [Vec3::ZERO; 2];
|
||||
for (kb, bk) in branch.iter_mut().enumerate() {
|
||||
@@ -764,7 +767,9 @@ fn nodal_quartic(
|
||||
cone: c,
|
||||
node,
|
||||
gradient: Vec3::new(g1x, g1y, g1z),
|
||||
ray: *branch.get(base).ok_or_else(sm_err)?,
|
||||
ray: *branch
|
||||
.get(base)
|
||||
.ok_or_else(|| sm_err("the base branch is not one of the node's two branches"))?,
|
||||
from,
|
||||
to,
|
||||
branch,
|
||||
@@ -838,9 +843,17 @@ pub fn calc_rational_spine_fillet_geom(
|
||||
SolutionRequested::Single,
|
||||
&GlobalPointSolveOptions::<crate::topology::ObjectHandle>::default(),
|
||||
)?;
|
||||
let t = sols.get(0).ok_or_else(sm_err)?.start.param(0)?;
|
||||
let t = sols
|
||||
.get(0)
|
||||
.ok_or_else(|| {
|
||||
sm_err("the trace's first point has no closest point on the edge's curve")
|
||||
})?
|
||||
.start
|
||||
.param(0)?;
|
||||
let pv = curve.evaluate(k, t, 1, true, false)?;
|
||||
pv.get(1).copied().ok_or_else(sm_err)?
|
||||
pv.get(1).copied().ok_or_else(|| {
|
||||
sm_err("the edge's curve has no first derivative at the trace's first point")
|
||||
})?
|
||||
};
|
||||
|
||||
let s = ops.solver();
|
||||
@@ -911,7 +924,7 @@ pub fn calc_rational_spine_fillet_geom(
|
||||
};
|
||||
let nf = (span.abs() / dmax).ceil();
|
||||
if !nf.is_finite() || nf > MAX_SPINE_SEGMENTS {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the spine needs too many segments"));
|
||||
}
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let mut n = nf as usize;
|
||||
@@ -937,7 +950,7 @@ pub fn calc_rational_spine_fillet_geom(
|
||||
let to_node = to_pt.length() <= tol;
|
||||
if from_node && to_node {
|
||||
// the trace starts and ends at the node
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the trace starts and ends at the node"));
|
||||
}
|
||||
let mut arc_q = nodal_quartic(&o0, &o1, blend.point, from_pt, to_pt, 0)?;
|
||||
// the arc from start to end that keeps off the node: G (g1.v) is not zero inside
|
||||
@@ -948,7 +961,10 @@ pub fn calc_rational_spine_fillet_geom(
|
||||
let end_branch = cand / 2;
|
||||
let sign = if cand % 2 == 0 { 1.0 } else { -1.0 };
|
||||
cand += 1;
|
||||
let eb = *arc_q.branch.get(end_branch).ok_or_else(sm_err)?;
|
||||
let eb = *arc_q
|
||||
.branch
|
||||
.get(end_branch)
|
||||
.ok_or_else(|| sm_err("the end branch is not one of the node's two branches"))?;
|
||||
let from = if from_node { eb } else { from_pt } * sign;
|
||||
let to = if to_node { eb } else { to_pt };
|
||||
if !from_node && !to_node && end_branch == 1 {
|
||||
@@ -979,7 +995,9 @@ pub fn calc_rational_spine_fillet_geom(
|
||||
}
|
||||
if !found {
|
||||
// the quartic's arc between the trace's ends runs through its node
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the quartic's arc between the trace's ends runs through its node",
|
||||
));
|
||||
}
|
||||
// s runs from the trace's first point to its last
|
||||
let mut s0 = 0.0;
|
||||
@@ -1044,7 +1062,7 @@ fn rational_spine_parts(
|
||||
let (w0, wd) = (at(&sg.w, 0), at(&sg.w, d));
|
||||
if !positive(w0) || !positive(wd) {
|
||||
// a spine segment's end weight is not positive
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("a spine segment's end weight is not positive"));
|
||||
}
|
||||
let c = (w0 / wd).powf(1.0 / d as f64);
|
||||
let mut ci = prev / w0;
|
||||
@@ -1110,7 +1128,7 @@ fn rational_spine_parts(
|
||||
if !all_positive {
|
||||
if depth >= 6 {
|
||||
// the round's weights do not keep positive
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the round's weights do not keep positive"));
|
||||
}
|
||||
let mut l = Segment::default();
|
||||
let mut rr = Segment::default();
|
||||
@@ -1151,7 +1169,9 @@ fn rational_spine_parts(
|
||||
let nv = rows_w.first().map_or(0, Vec::len);
|
||||
let nseg = rows_w.len();
|
||||
if nv < 2 || nseg == 0 || deg == 0 {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the round has too few section points, no spine segments or no degree",
|
||||
));
|
||||
}
|
||||
let nu = nseg * deg + 1;
|
||||
let mut cps = vec![Point3::ZERO; nu * nv];
|
||||
@@ -1202,7 +1222,7 @@ fn rational_spine_parts(
|
||||
}
|
||||
if !positive(wmin) || wmax > 1e4 * wmin {
|
||||
// the round's weights range too far for a NURBS
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the round's weights range too far for a NURBS"));
|
||||
}
|
||||
let sc = 1.0 / (wmin * wmax).sqrt();
|
||||
for w in &mut wts {
|
||||
@@ -1227,7 +1247,7 @@ fn rational_spine_parts(
|
||||
// the round is the ball's: its rails on their faces, every point at r from the spine
|
||||
for (kk, sd) in done.iter().enumerate() {
|
||||
let (Some(&ua), Some(&ub)) = (u_knots.get(kk), u_knots.get(kk + 1)) else {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("a spine segment has no knot span in the round"));
|
||||
};
|
||||
for a in 0..=4 {
|
||||
let t = f64::from(a) / 4.0;
|
||||
@@ -1246,7 +1266,7 @@ fn rational_spine_parts(
|
||||
};
|
||||
if off > tol || on_face > tol {
|
||||
// the round built is not the ball's
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the round built is not the ball's"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1266,7 +1286,9 @@ fn rational_spine_parts(
|
||||
*slot = Some(c.natural_interval());
|
||||
rails.push(c);
|
||||
}
|
||||
let [rail0, rail1]: [Curve; 2] = rails.try_into().map_err(|_| sm_err())?;
|
||||
let [rail0, rail1]: [Curve; 2] = rails
|
||||
.try_into()
|
||||
.map_err(|_| sm_err("the round's iso-curves do not give two rails"))?;
|
||||
let spine = {
|
||||
let sdeg = done.first().map_or(0, |d| degree(&d.w));
|
||||
let mut sp: Vec<Point3> = Vec::new();
|
||||
@@ -1381,6 +1403,42 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A plane that meets the quadric in no conic says how it fails, with status `SM_ERR`.
|
||||
#[test]
|
||||
fn a_plane_missing_the_quadric_or_meeting_it_in_lines_says_so() {
|
||||
let quadric = |kind: QuadricKind| BlendQuadric {
|
||||
kind,
|
||||
point: Point3::ZERO,
|
||||
dir: Vec3::new(0.0, 0.0, 1.0),
|
||||
radius: 4.0,
|
||||
angle: 0.0,
|
||||
side: 1.0,
|
||||
tube: 0.0,
|
||||
};
|
||||
let missed = plane_section(
|
||||
Point3::new(0.0, 0.0, 10.0),
|
||||
Vec3::new(0.0, 0.0, 1.0),
|
||||
&quadric(QuadricKind::Sphere),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
missed,
|
||||
SolidError::blend_failed(OP, "the plane misses the quadric")
|
||||
);
|
||||
assert_eq!(missed.sm_status(), Some(crate::error::SM_ERR));
|
||||
// a plane along the cylinder's axis, outside it
|
||||
let lines = plane_section(
|
||||
Point3::new(10.0, 0.0, 0.0),
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
&quadric(QuadricKind::Cylinder),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
lines.to_string(),
|
||||
"blend::fillet_exact::rational: the plane meets the quadric in lines"
|
||||
);
|
||||
}
|
||||
|
||||
/// The original nodal pair satisfies both offset equations, including its last end.
|
||||
#[test]
|
||||
fn two_touching_cylinders_meet_in_a_nodal_quartic_its_segments_on_both() {
|
||||
|
||||
@@ -52,14 +52,15 @@ use super::FilletObject;
|
||||
|
||||
const OP: &str = "blend::fillet_executive::session::ease";
|
||||
|
||||
/// `SM_ERR`: the setback's sections cannot be eased.
|
||||
fn sm_err() -> SolidError {
|
||||
SolidError::blend_failed(OP, "the setback's sections cannot be eased")
|
||||
/// `SM_ERR`: the setback's sections cannot be eased, as `what` says.
|
||||
fn sm_err(what: &'static str) -> SolidError {
|
||||
SolidError::blend_failed(OP, what)
|
||||
}
|
||||
|
||||
/// `SM_ERR_NULL_POINTER` (`NER`): an object easing the setback's sections needs.
|
||||
fn null() -> SolidError {
|
||||
SolidError::blend_missing(OP, "an object easing the setback's sections needs")
|
||||
/// `SM_ERR_NULL_POINTER` (`NER`): `what`, which easing the setback's sections needs, is
|
||||
/// missing.
|
||||
fn null(what: &'static str) -> SolidError {
|
||||
SolidError::blend_missing(OP, what)
|
||||
}
|
||||
|
||||
/// The B-spline part of a curve (`SM_CAST_PTR(SmBSplineCurve, ..)`): the kinds derived from
|
||||
@@ -134,7 +135,9 @@ impl SessionObjects<'_> {
|
||||
if !(corner.set_back_dist() > 0.0) || !(corner.ease_dist() > 0.0) {
|
||||
continue;
|
||||
}
|
||||
let cp_vertex: VertexId = corner.filleted_vertex().ok_or_else(null)?;
|
||||
let cp_vertex: VertexId = corner
|
||||
.filleted_vertex()
|
||||
.ok_or_else(|| null("the corner's filleted vertex"))?;
|
||||
// the corner's setback along each of its edges (GetSetBackDistAlong), read while
|
||||
// the surfaces change
|
||||
let ease_dist = corner.ease_dist();
|
||||
@@ -153,37 +156,53 @@ impl SessionObjects<'_> {
|
||||
continue;
|
||||
}
|
||||
if !vertex.is_processed() {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"a setback vertex of the eased corner is not processed",
|
||||
));
|
||||
}
|
||||
let class_object = vertex.point_class_object();
|
||||
let fv_geoms = self.pseudo.vertex_fillet_geoms(fv)?;
|
||||
if fv_geoms.len() != 1 {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the setback vertex is not on exactly one fillet geometry",
|
||||
));
|
||||
}
|
||||
let fg = *fv_geoms.first().ok_or_else(sm_err)?;
|
||||
let solver_id = self.pseudo.geom(fg)?.solver.ok_or_else(null)?;
|
||||
let fg = *fv_geoms
|
||||
.first()
|
||||
.ok_or_else(|| sm_err("the setback vertex has no fillet geometry"))?;
|
||||
let solver_id = self
|
||||
.pseudo
|
||||
.geom(fg)?
|
||||
.solver
|
||||
.ok_or_else(|| null("the round's solver"))?;
|
||||
let base = self.solver(solver_id)?.base();
|
||||
let fsg_type = base
|
||||
.fillet_surface_generator()
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the round solver's surface generator"))?
|
||||
.fillet_surface_generator_type();
|
||||
if fsg_type == FilletSurfaceGeneratorType::BlendCurve {
|
||||
continue; // its sections are curvature-continuous already
|
||||
}
|
||||
if fsg_type != FilletSurfaceGeneratorType::Circular {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the round's sections are neither circular nor curvature-continuous",
|
||||
));
|
||||
}
|
||||
// SM_CAST_PTR(SmFace, pFV->GetPointClassObject()), NER
|
||||
let Some(EntityRef::Face(face)) = class_object else {
|
||||
return Err(null());
|
||||
return Err(null("the face the setback vertex classifies on"));
|
||||
};
|
||||
let rail_index = base.find_index_of_rail_on_face(self.target, Some(face))?;
|
||||
if rail_index > 1 {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the setback vertex's face carries neither rail"));
|
||||
}
|
||||
let rail = self.pseudo.geom(fg)?.rail(rail_index).ok_or_else(null)?;
|
||||
let rail = self
|
||||
.pseudo
|
||||
.geom(fg)?
|
||||
.rail(rail_index)
|
||||
.ok_or_else(|| null("the round's rail on the setback vertex's face"))?;
|
||||
let rail_curve = edge::curve(self.pseudo.brep(), rail)?
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the rail's curve"))?
|
||||
.clone();
|
||||
let tol = base.this_approx_tol_3d();
|
||||
let rail_ivl = rail_curve.natural_interval();
|
||||
@@ -199,22 +218,26 @@ impl SessionObjects<'_> {
|
||||
SolverOperation::Minimize,
|
||||
)?;
|
||||
if !dropped.success {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the setback vertex does not drop onto the rail"));
|
||||
}
|
||||
let to = dropped.parameter;
|
||||
|
||||
// w = 0 where the plane square to the edge, the ease farther from the
|
||||
// vertex, meets the rail (as SmFilletVertex::CalcSetBackVert finds the
|
||||
// setback's)
|
||||
let edgeuse = base.edgeuse(0)?.ok_or_else(null)?;
|
||||
let e = eu::edge(self.target, edgeuse)?.ok_or_else(null)?;
|
||||
let edgeuse = base
|
||||
.edgeuse(0)?
|
||||
.ok_or_else(|| null("the round solver's first edge use"))?;
|
||||
let e = eu::edge(self.target, edgeuse)?
|
||||
.ok_or_else(|| null("the first edge use's edge"))?;
|
||||
// each edge's own setback (the fork's 61547af, GetSetBackDistAlong)
|
||||
let dist_at = along_edge
|
||||
.iter()
|
||||
.find(|(edge, _)| *edge == e)
|
||||
.map_or(set_back_dist, |&(_, d)| d)
|
||||
+ ease_dist;
|
||||
let edge_curve = edge::curve(self.target, e)?.ok_or_else(null)?;
|
||||
let edge_curve = edge::curve(self.target, e)?
|
||||
.ok_or_else(|| null("the filleted edge's curve"))?;
|
||||
let edge_ivl = edge::interval(self.target, e)?;
|
||||
let from_start = edge::start_vertex(self.target, e)? == Some(cp_vertex);
|
||||
let edge_param = arc_length_param(
|
||||
@@ -229,20 +252,25 @@ impl SessionObjects<'_> {
|
||||
Some(edge::tolerance(self.target, e)?),
|
||||
)?;
|
||||
if !edge_ivl.contains_value(edge_param, 0.0) {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the ease runs past the end of the filleted edge"));
|
||||
}
|
||||
let pv = edge_curve.evaluate(k, edge_param, 1, true, true)?;
|
||||
let plane_orig = pv.first().copied().ok_or_else(sm_err)?;
|
||||
let plane_normal = pv.get(1).copied().ok_or_else(sm_err)?;
|
||||
let plane_orig = pv
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| sm_err("the edge's curve has no point at the eased setback"))?;
|
||||
let plane_normal = pv.get(1).copied().ok_or_else(|| {
|
||||
sm_err("the edge's curve has no tangent at the eased setback")
|
||||
})?;
|
||||
let surf1 = Surface::from(
|
||||
base.surface(0)?
|
||||
.map(|slot| slot.surface.surface.clone())
|
||||
.ok_or_else(null)?,
|
||||
.ok_or_else(|| null("the round solver's first surface"))?,
|
||||
);
|
||||
let surf2 = Surface::from(
|
||||
base.surface(1)?
|
||||
.map(|slot| slot.surface.surface.clone())
|
||||
.ok_or_else(null)?,
|
||||
.ok_or_else(|| null("the round solver's second surface"))?,
|
||||
);
|
||||
let domain1 = surf1.natural_uv_domain_with_nurb(k)?;
|
||||
let domain2 = surf2.natural_uv_domain_with_nurb(k)?;
|
||||
@@ -258,9 +286,13 @@ impl SessionObjects<'_> {
|
||||
SolutionRequested::All,
|
||||
)?;
|
||||
if solutions.len() != 1 {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the plane's origin has no single closest point on an offset surface",
|
||||
));
|
||||
}
|
||||
let s = solutions.get(0).ok_or_else(sm_err)?;
|
||||
let s = solutions.get(0).ok_or_else(|| {
|
||||
sm_err("the closest point on an offset surface was not returned")
|
||||
})?;
|
||||
Ok(Point2::new(s.start.param(0)?, s.start.param(1)?))
|
||||
};
|
||||
let uv1 = closest(&surf1, &domain1)?;
|
||||
@@ -280,7 +312,9 @@ impl SessionObjects<'_> {
|
||||
&mut tsect,
|
||||
)?;
|
||||
if !found {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"no section of the round lies in the plane at the eased setback",
|
||||
));
|
||||
}
|
||||
let srf = if rail_index == 0 {
|
||||
SrfIndex::First
|
||||
@@ -288,7 +322,7 @@ impl SessionObjects<'_> {
|
||||
SrfIndex::Second
|
||||
};
|
||||
let start = face2::surface(self.target, face)?
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the setback vertex face's surface"))?
|
||||
.evaluate_point(k, tsect.uv_pos(srf))?;
|
||||
let dropped = rail_curve.drop_point(
|
||||
k,
|
||||
@@ -301,7 +335,7 @@ impl SessionObjects<'_> {
|
||||
)?;
|
||||
let from = dropped.parameter;
|
||||
if !dropped.success || (from - to).abs() <= EFF_ZERO {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the ease start does not drop onto the rail, or drops where the setback's section does"));
|
||||
}
|
||||
|
||||
let index = if let Some(index) = geoms.iter().position(|&g| g == fg) {
|
||||
@@ -313,23 +347,29 @@ impl SessionObjects<'_> {
|
||||
};
|
||||
ramps
|
||||
.get_mut(index)
|
||||
.ok_or_else(sm_err)?
|
||||
.ok_or_else(|| sm_err("the round has no ramp list"))?
|
||||
.push(EaseRamp { from, to });
|
||||
}
|
||||
}
|
||||
|
||||
for (&fg, geom_ramps) in geoms.iter().zip(&ramps) {
|
||||
let solver_id = self.pseudo.geom(fg)?.solver.ok_or_else(null)?;
|
||||
let surface = self.fillet_surface(fg)?.ok_or_else(null)?;
|
||||
let solver_id = self
|
||||
.pseudo
|
||||
.geom(fg)?
|
||||
.solver
|
||||
.ok_or_else(|| null("the eased round's solver"))?;
|
||||
let surface = self
|
||||
.fillet_surface(fg)?
|
||||
.ok_or_else(|| null("the eased round's fillet surface"))?;
|
||||
let own_surface = surface.clone();
|
||||
let own = crate::topology::brep::p3::with_nurb(k, surface)?
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the eased round surface's B-spline part"))?
|
||||
.into_owned();
|
||||
let center = self
|
||||
.pseudo
|
||||
.geom(fg)?
|
||||
.center_line_curve()
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the eased round's centre line"))?
|
||||
.clone();
|
||||
let base = self.solver(solver_id)?.base();
|
||||
let tol = base.this_approx_tol_3d();
|
||||
@@ -337,24 +377,33 @@ impl SessionObjects<'_> {
|
||||
let mut uvs: Vec<BSplineCurve> = Vec::new();
|
||||
let mut face_surfs: Vec<Surface> = Vec::new();
|
||||
for r in 0..2 {
|
||||
let rail_edge = self.pseudo.geom(fg)?.rail(r).ok_or_else(null)?;
|
||||
let rail_edge = self
|
||||
.pseudo
|
||||
.geom(fg)?
|
||||
.rail(r)
|
||||
.ok_or_else(|| null("a rail of the eased round"))?;
|
||||
let rail = edge::curve(self.pseudo.brep(), rail_edge)?
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the eased round rail's curve"))?
|
||||
.clone();
|
||||
if bspline_curve(&rail).is_none() {
|
||||
return Err(null());
|
||||
return Err(null("the eased round rail's B-spline part"));
|
||||
}
|
||||
// the face the rail rolls on (the solver's surface r is its offset)
|
||||
let edgeuse = base.edgeuse(r)?.ok_or_else(null)?;
|
||||
let rail_face = eu::face(self.target, edgeuse)?.ok_or_else(null)?;
|
||||
let edgeuse = base
|
||||
.edgeuse(r)?
|
||||
.ok_or_else(|| null("the eased round solver's edge use"))?;
|
||||
let rail_face = eu::face(self.target, edgeuse)?
|
||||
.ok_or_else(|| null("the face of the eased round solver's edge use"))?;
|
||||
let face_surf = face2::surface(self.target, rail_face)?
|
||||
.ok_or_else(null)?
|
||||
.ok_or_else(|| null("the surface of the face the rail rolls on"))?
|
||||
.clone();
|
||||
// the rail on that face's surface: as the trace left it, or (an analytic
|
||||
// round, whose rails are lines) the rail dropped onto it, over the rail's
|
||||
// own parameter
|
||||
let uv = if let Some(curve) = self.pseudo.original_uv_curve(rail_edge)? {
|
||||
bspline_curve(curve).ok_or_else(null)?.clone()
|
||||
bspline_curve(curve)
|
||||
.ok_or_else(|| null("the rail's original UV curve's B-spline part"))?
|
||||
.clone()
|
||||
} else {
|
||||
let uv_curves = face_surf
|
||||
.drop_curve::<NoObject>(
|
||||
@@ -369,24 +418,32 @@ impl SessionObjects<'_> {
|
||||
)?
|
||||
.uv_curves;
|
||||
if uv_curves.len() != 1 {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the rail does not drop onto its face's surface as one UV curve",
|
||||
));
|
||||
}
|
||||
let first = uv_curves.first().ok_or_else(sm_err)?;
|
||||
bspline_curve(first).ok_or_else(null)?.clone()
|
||||
let first = uv_curves
|
||||
.first()
|
||||
.ok_or_else(|| sm_err("the rail's dropped UV curve was not returned"))?;
|
||||
bspline_curve(first)
|
||||
.ok_or_else(|| null("the rail's dropped UV curve's B-spline part"))?
|
||||
.clone()
|
||||
};
|
||||
let uv_ivl = uv.natural_interval();
|
||||
let rail_ivl = rail.natural_interval();
|
||||
if (uv_ivl.min() - rail_ivl.min()).abs() > 1.0e-9 * (1.0 + rail_ivl.length())
|
||||
|| (uv_ivl.max() - rail_ivl.max()).abs() > 1.0e-9 * (1.0 + rail_ivl.length())
|
||||
{
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"the rail's UV curve does not run over the rail's own interval",
|
||||
));
|
||||
}
|
||||
rails.push(rail);
|
||||
uvs.push(uv);
|
||||
face_surfs.push(face_surf);
|
||||
}
|
||||
let (Some(rail0), Some(rail1)) = (rails.first(), rails.get(1)) else {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the eased round does not have two rails"));
|
||||
};
|
||||
|
||||
// the rails' direction on the surface: the one over the rails' interval whose
|
||||
@@ -411,7 +468,10 @@ impl SessionObjects<'_> {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let t = geom_ramps.first().ok_or_else(sm_err)?.to;
|
||||
let t = geom_ramps
|
||||
.first()
|
||||
.ok_or_else(|| sm_err("the eased round has no ramp"))?
|
||||
.to;
|
||||
let on_rail = rail0.evaluate_point(k, t)?;
|
||||
let lo = own_surface.evaluate_point(k, uv_at(dir, t, across_ivl.min()))?;
|
||||
let hi = own_surface.evaluate_point(k, uv_at(dir, t, across_ivl.max()))?;
|
||||
@@ -421,23 +481,29 @@ impl SessionObjects<'_> {
|
||||
}
|
||||
}
|
||||
if along == SurfParamType::Unknown {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err(
|
||||
"neither surface direction runs along the eased round's rails",
|
||||
));
|
||||
}
|
||||
|
||||
// the curvature-continuous surface on the same rails (CreateSurfaceFromCurves
|
||||
// changes the curves' knots: it is given copies)
|
||||
let c0 = center;
|
||||
let mut c1 = bspline_curve(rail0).ok_or_else(null)?.clone();
|
||||
let mut c2 = bspline_curve(rail1).ok_or_else(null)?.clone();
|
||||
let mut c1 = bspline_curve(rail0)
|
||||
.ok_or_else(|| null("the first rail's B-spline part"))?
|
||||
.clone();
|
||||
let mut c2 = bspline_curve(rail1)
|
||||
.ok_or_else(|| null("the second rail's B-spline part"))?
|
||||
.clone();
|
||||
let (Some(surf0), Some(surf1)) = (face_surfs.first(), face_surfs.get(1)) else {
|
||||
return Err(sm_err());
|
||||
return Err(sm_err("the eased round does not have two rail faces"));
|
||||
};
|
||||
// the fork's order-2 G^n section (WR-685, tangency-labs/smlib@11ea2d9): the
|
||||
// faces' curvature across the rails. Its thumbweight stays 0, not the equal area
|
||||
// default: it is the corner's transition, not the blend's own section (the fork's
|
||||
// 22d258a = d01d7b0's adb9854)
|
||||
let (Some(uv0), Some(uv1)) = (uvs.first(), uvs.get(1)) else {
|
||||
return Err(null());
|
||||
return Err(null("the eased round's two UV curves"));
|
||||
};
|
||||
let g2 = GnSectionFsg::new(2, 0.0).create_surface_from_curves(
|
||||
k,
|
||||
@@ -460,3 +526,26 @@ impl SessionObjects<'_> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::{SM_ERR, SM_ERR_NULL_POINTER};
|
||||
|
||||
/// Each ease failure names what was missing or what failed, and keeps its status.
|
||||
#[test]
|
||||
fn ease_failures_name_what_failed_and_keep_their_status() {
|
||||
let missing = null("the corner's filleted vertex");
|
||||
assert_eq!(
|
||||
missing.to_string(),
|
||||
"blend::fillet_executive::session::ease: the corner's filleted vertex is missing"
|
||||
);
|
||||
assert_eq!(missing.sm_status(), Some(SM_ERR_NULL_POINTER));
|
||||
let failed = sm_err("the ease runs past the end of the filleted edge");
|
||||
assert_eq!(
|
||||
failed.to_string(),
|
||||
"blend::fillet_executive::session::ease: the ease runs past the end of the filleted edge"
|
||||
);
|
||||
assert_eq!(failed.sm_status(), Some(SM_ERR));
|
||||
}
|
||||
}
|
||||
|
||||
+206
-138
@@ -147,7 +147,7 @@
|
||||
//! are ignored here too (`Unitize` of a zero vector yields `(0, 0, 1)`, a failed
|
||||
//! interval intersection the "inside out" interval), except that an
|
||||
//! [`SolidError::Unported`] is always propagated. Null dereferences upstream
|
||||
//! (missing rails, curves, uses) are [`SolidError::Upstream`] with
|
||||
//! (missing rails, curves, uses) are blend failures with
|
||||
//! `SM_ERR_NULL_POINTER`; a point classification object of the wrong kind where
|
||||
//! upstream casts blindly is [`SolidError::InvalidInput`].
|
||||
//! - **Record writes.** `SmVertex::SetPoint` (in `CalcVertOnVert`,
|
||||
@@ -201,6 +201,7 @@ use crate::blend::fillet_solver::p1::{FilletSolver, FilletSolverOps};
|
||||
use crate::curve::curve::p1::GlobalPointSolveOptions;
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::curve::hermite_curve::HermiteCurve;
|
||||
use crate::error::blend::BlendFailure;
|
||||
use crate::error::{EntityRef, Result, SolidError};
|
||||
use crate::geom::extent1d::Extent1d;
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
@@ -262,13 +263,6 @@ const OP_SET_OFFSET: &str = "blend::fillet_geom::p1::FilletGeom::set_offset_surf
|
||||
|
||||
/// `SM_ERR_BAD_INTERSECTIONS`.
|
||||
pub use crate::error::SM_ERR_BAD_INTERSECTIONS;
|
||||
fn upstream(operation: &'static str, code: i32) -> SolidError {
|
||||
SolidError::Upstream {
|
||||
operation,
|
||||
code,
|
||||
entity: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `SM_DEG2RAD`.
|
||||
fn deg2rad(deg: f64) -> f64 {
|
||||
@@ -690,7 +684,7 @@ impl<'c> CornerLookup<'c> {
|
||||
// the NULL its NER would return for
|
||||
self.table
|
||||
.and_then(|table| usize::try_from(id.0).ok().and_then(|i| table.get(i)))
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_CORNER))
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_CORNER, "the corner"))
|
||||
}
|
||||
|
||||
/// `pCorner->GetExtendedSurface(pOriginalFace)`
|
||||
@@ -1436,7 +1430,7 @@ impl FilletGeom {
|
||||
Some(FilletSurfaceStorage::ManagedFace { body, face }) => {
|
||||
let owner = managed_owner(owner, *body)?;
|
||||
Ok(Some(owner.face(*face)?.surface.as_ref().ok_or_else(
|
||||
|| SolidError::null_pointer(OP_FILLET_SURFACE),
|
||||
|| SolidError::blend_missing(OP_FILLET_SURFACE, "the fillet face's surface"),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
@@ -1457,7 +1451,7 @@ impl FilletGeom {
|
||||
Some(FilletSurfaceStorage::ManagedFace { body, face }) => {
|
||||
let owner = managed_owner(owner, *body)?;
|
||||
Ok(Some(owner.face_mut(*face)?.surface.as_mut().ok_or_else(
|
||||
|| SolidError::null_pointer(OP_FILLET_SURFACE),
|
||||
|| SolidError::blend_missing(OP_FILLET_SURFACE, "the fillet face's surface"),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
@@ -1476,7 +1470,7 @@ impl FilletGeom {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) when the fallback is needed and
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) when the fallback is needed and
|
||||
/// `solver` is `None` (upstream dereferences `NULL`); [`SolidError::InvalidInput`]
|
||||
/// for a rail index past the two rails (upstream reads out of bounds).
|
||||
pub fn offset_surface<'a>(
|
||||
@@ -1491,7 +1485,8 @@ impl FilletGeom {
|
||||
if let Some(s) = own {
|
||||
return Ok(Some(s));
|
||||
}
|
||||
let fs = solver.ok_or_else(|| SolidError::null_pointer(OP_OFFSET))?;
|
||||
let fs =
|
||||
solver.ok_or_else(|| SolidError::blend_missing(OP_OFFSET, "the geometry's solver"))?;
|
||||
Ok(fs.surface(rail_index)?.map(|slot| &slot.surface))
|
||||
}
|
||||
|
||||
@@ -1677,15 +1672,18 @@ impl FilletBrep {
|
||||
/// # Errors
|
||||
///
|
||||
/// The errors of `SmBrep::MakeShellVertexTopology` when the region has no shell;
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) without an infinite region
|
||||
/// (upstream dereferences it; `SmBrep()` always makes one); [`SolidError::Upstream`]
|
||||
/// a blend failure (`SM_ERR_NULL_POINTER`) without an infinite region
|
||||
/// (upstream dereferences it; `SmBrep()` always makes one); a blend failure
|
||||
/// (`SM_ERR_NULL_POINTER`) when the region still has no shell (upstream reads
|
||||
/// `sShells[0]` of an empty array).
|
||||
pub fn new_fillet_edgeuse(&mut self) -> Result<EdgeuseId> {
|
||||
// upstream dereferences m_pInfiniteRegion unchecked: SmFilletBrep() runs SmBrep(),
|
||||
// which always creates the infinite region, so it is never NULL there
|
||||
let Some(reg) = infinite_region(&self.brep) else {
|
||||
return Err(SolidError::null_pointer(OP_EDGEUSE));
|
||||
return Err(SolidError::blend_missing(
|
||||
OP_EDGEUSE,
|
||||
"the fillet B-rep's infinite region",
|
||||
));
|
||||
};
|
||||
let mut shells = region_shells(&self.brep, reg)?;
|
||||
if shells.is_empty() {
|
||||
@@ -1695,7 +1693,7 @@ impl FilletBrep {
|
||||
}
|
||||
let shell = *shells
|
||||
.first()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EDGEUSE))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EDGEUSE, "the infinite region's shell"))?;
|
||||
// SmEdgeuse(), then m_pSorLU = sShells[0], m_tEdgeuseType = SmShell_TYPE
|
||||
let eu = self.brep.add_edgeuse_record(Edgeuse {
|
||||
owner: EdgeuseOwner::Shell(Some(shell)),
|
||||
@@ -1781,7 +1779,10 @@ impl FilletBrep {
|
||||
)
|
||||
})?;
|
||||
let class = record.curve_class.as_mut().ok_or_else(|| {
|
||||
SolidError::null_pointer("blend::fillet_geom::with_rail_classification")
|
||||
SolidError::blend_missing(
|
||||
"blend::fillet_geom::with_rail_classification",
|
||||
"the fillet edge's curve classification",
|
||||
)
|
||||
})?;
|
||||
call(&self.brep, class)
|
||||
}
|
||||
@@ -1919,7 +1920,12 @@ impl FilletBrep {
|
||||
"classification object is not a vertex",
|
||||
))
|
||||
}
|
||||
None => return Err(SolidError::null_pointer(OP_VERT_ON_VERT)),
|
||||
None => {
|
||||
return Err(SolidError::blend_missing(
|
||||
OP_VERT_ON_VERT,
|
||||
"the classification object",
|
||||
))
|
||||
}
|
||||
};
|
||||
self.set_vertex_point(v, point)?;
|
||||
self.fillet_vertex_mut(v)?.set_status(FilStatus::Processed);
|
||||
@@ -1945,7 +1951,7 @@ impl FilletBrep {
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] when the classification object is not an edge use
|
||||
/// (upstream casts blindly); [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for
|
||||
/// (upstream casts blindly); a blend failure (`SM_ERR_NULL_POINTER`) for
|
||||
/// an edge without a curve; `SM_ERR` when no intersection is found; those of
|
||||
/// [`Surface::global_curve_intersect`].
|
||||
pub fn calc_cliff_rail_int_edge(
|
||||
@@ -1965,17 +1971,22 @@ impl FilletBrep {
|
||||
"classification object is not an edge use",
|
||||
))
|
||||
}
|
||||
None => return Err(SolidError::null_pointer(OP_CLIFF)),
|
||||
None => {
|
||||
return Err(SolidError::blend_missing(
|
||||
OP_CLIFF,
|
||||
"the classification object",
|
||||
))
|
||||
}
|
||||
};
|
||||
let edge = target
|
||||
.edgeuse(eu)?
|
||||
.edge
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_CLIFF))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_CLIFF, "the edge use's edge"))?;
|
||||
let curve_3d = target
|
||||
.edge(edge)?
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_CLIFF))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_CLIFF, "the edge's curve"))?;
|
||||
let ivl = curve_3d.natural_interval();
|
||||
|
||||
// get FilletSurface/OrigBrepCurve intersection marking this FilletVertex Position3d
|
||||
@@ -1990,17 +2001,17 @@ impl FilletBrep {
|
||||
&ivl,
|
||||
this_approx_tol_3d,
|
||||
)?;
|
||||
let first = solutions
|
||||
.get(0)
|
||||
.ok_or_else(|| SolidError::sm_err(OP_CLIFF))?;
|
||||
let first = solutions.get(0).ok_or_else(|| {
|
||||
SolidError::blend_failed(
|
||||
OP_CLIFF,
|
||||
"the fillet surface does not cross the edge's curve",
|
||||
)
|
||||
})?;
|
||||
|
||||
// side effect: Set SmFilletVertex::m_vPoint = XSectPoint(FilletSurface, Orig BrepCurve)
|
||||
let t = first
|
||||
.start
|
||||
.parameters
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::sm_err(OP_CLIFF))?;
|
||||
let t = first.start.parameters.first().copied().ok_or_else(|| {
|
||||
SolidError::blend_failed(OP_CLIFF, "the crossing has no curve parameter")
|
||||
})?;
|
||||
let pnt = curve_3d.evaluate_point(k, t)?;
|
||||
self.set_vertex_point(v, pnt)?;
|
||||
self.fillet_vertex_mut(v)?.set_status(FilStatus::Processed);
|
||||
@@ -2055,18 +2066,18 @@ impl FilletBrep {
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] for dangling ids or a use that is not a fillet record;
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a vertex use without an
|
||||
/// a blend failure (`SM_ERR_NULL_POINTER`) for a vertex use without an
|
||||
/// edge use or an edge use without an edge.
|
||||
pub fn vertex_fillet_geoms(&self, v: VertexId) -> Result<Vec<FilletGeomId>> {
|
||||
let mut geoms = Vec::new();
|
||||
for vu in vertexuses(&self.brep, v)? {
|
||||
let eu = eu1::vertexuse_edgeuse(&self.brep, vu)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_VERTEX))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_VERTEX, "the vertex use's edge use"))?;
|
||||
let e = self
|
||||
.brep
|
||||
.edgeuse(eu)?
|
||||
.edge
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_VERTEX))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_VERTEX, "the edge use's edge"))?;
|
||||
|
||||
// skip edges not of type SM_FE_RAIL
|
||||
if self.fillet_edge(e)?.fillet_edge_type() != FilletEdgeType::Rail {
|
||||
@@ -2098,12 +2109,12 @@ impl FilletBrep {
|
||||
) -> Result<Option<VertexuseId>> {
|
||||
for vu in vertexuses(&self.brep, v)? {
|
||||
let eu = eu1::vertexuse_edgeuse(&self.brep, vu)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_VERTEX))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_VERTEX, "the vertex use's edge use"))?;
|
||||
let e = self
|
||||
.brep
|
||||
.edgeuse(eu)?
|
||||
.edge
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_VERTEX))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_VERTEX, "the edge use's edge"))?;
|
||||
let edge_type = self.fillet_edge(e)?.fillet_edge_type();
|
||||
|
||||
// when edge is a RAIL or CLIFF_RAIL type
|
||||
@@ -2203,7 +2214,7 @@ impl FilletBrep {
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::NotFound`] when `e` is not a fillet edge or an id dangles;
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for an edge use without a
|
||||
/// a blend failure (`SM_ERR_NULL_POINTER`) for an edge use without a
|
||||
/// vertex use or a vertex use without a vertex.
|
||||
pub fn delete_fillet_edge(&mut self, e: EdgeId) -> Result<()> {
|
||||
self.fillet_edge(e)?;
|
||||
@@ -2211,16 +2222,15 @@ impl FilletBrep {
|
||||
// delete all connected edgeuses and the edgeuses' connected vertexuses
|
||||
let uses = edge_unit::edgeuses(&self.brep, e, None, None)?;
|
||||
for eu in uses {
|
||||
let vu = self
|
||||
.brep
|
||||
.edgeuse(eu)?
|
||||
.vertexuse
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EDGE))?;
|
||||
let vu =
|
||||
self.brep.edgeuse(eu)?.vertexuse.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EDGE, "the edge use's vertex use")
|
||||
})?;
|
||||
let v = self
|
||||
.brep
|
||||
.vertexuse(vu)?
|
||||
.vertex
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EDGE))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EDGE, "the vertex use's vertex"))?;
|
||||
self.brep.vertex_mut(v)?.vertexuses.retain(|&x| x != vu);
|
||||
// delete pEU (~SmTopology: unlinked from the edge's list)
|
||||
self.brep.edge_mut(e)?.edgeuses.retain(|&x| x != eu);
|
||||
@@ -2249,7 +2259,7 @@ impl FilletBrep {
|
||||
pub fn delete_wire_fillet_edge(&mut self, k: &mut Kernel, e: EdgeId) -> Result<()> {
|
||||
// check state - m_bEditingEnabled must be on
|
||||
if !self.editing_enabled {
|
||||
return Err(SolidError::sm_err(OP_EDGE));
|
||||
return Err(SolidError::blend_failed(OP_EDGE, "editing is disabled"));
|
||||
}
|
||||
self.fillet_edge(e)?;
|
||||
let uses = edge_unit::edgeuses(&self.brep, e, None, None)?;
|
||||
@@ -2285,13 +2295,13 @@ impl FilletBrep {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for an edge without a primary
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) for an edge without a primary
|
||||
/// edge use or a primary use without a mate.
|
||||
pub fn original_uv_curve(&self, e: EdgeId) -> Result<Option<&Curve>> {
|
||||
let primary =
|
||||
primary_edgeuse(&self.brep, e)?.ok_or_else(|| SolidError::null_pointer(OP_EDGE))?;
|
||||
let mate_eu =
|
||||
mate(&self.brep, primary)?.ok_or_else(|| SolidError::null_pointer(OP_EDGE))?;
|
||||
let primary = primary_edgeuse(&self.brep, e)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EDGE, "the edge's primary edge use"))?;
|
||||
let mate_eu = mate(&self.brep, primary)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EDGE, "the primary edge use's mate"))?;
|
||||
Ok(self.brep.edgeuse(mate_eu)?.uv_trim_curve.as_ref())
|
||||
}
|
||||
|
||||
@@ -2304,7 +2314,7 @@ impl FilletBrep {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for an edge without a curve,
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) for an edge without a curve,
|
||||
/// `SM_ERR` when no edge matches; those of the curve evaluations and point solve.
|
||||
pub fn find_fillet_brep_edge(
|
||||
&mut self,
|
||||
@@ -2317,12 +2327,10 @@ impl FilletBrep {
|
||||
self.fillet_edge(e)?;
|
||||
|
||||
// get this Fillet Edge's curve
|
||||
let curve_3d = self
|
||||
.brep
|
||||
.edge(e)?
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_FIND_EDGE))?;
|
||||
let curve_3d =
|
||||
self.brep.edge(e)?.curve.as_ref().ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_FIND_EDGE, "the fillet edge's curve")
|
||||
})?;
|
||||
if curve_3d.is_degenerate(k, EFF_ZERO, None)? {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -2346,10 +2354,9 @@ impl FilletBrep {
|
||||
// First just look for a matching midpoint.
|
||||
for &pe in edges {
|
||||
let edge = fillet_brep.edge(pe)?;
|
||||
let edge_curve = edge
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_FIND_EDGE))?;
|
||||
let edge_curve = edge.curve.as_ref().ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_FIND_EDGE, "the fillet B-rep edge's curve")
|
||||
})?;
|
||||
|
||||
// let sEdgePnt = edge midPoint
|
||||
let edge_pnt = edge_curve.evaluate_point(k, edge.interval.evaluate(0.5))?;
|
||||
@@ -2421,10 +2428,9 @@ impl FilletBrep {
|
||||
{
|
||||
if let Some(best) = best_edge1.filter(|&b| Some(b) != record.fillet_brep_edge1) {
|
||||
let edge = fillet_brep.edge(best)?;
|
||||
let edge_curve = edge
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_FIND_EDGE))?;
|
||||
let edge_curve = edge.curve.as_ref().ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_FIND_EDGE, "the fillet B-rep edge's curve")
|
||||
})?;
|
||||
let mut on_curve = true;
|
||||
for end in 0..2 {
|
||||
if !on_curve {
|
||||
@@ -2469,7 +2475,10 @@ impl FilletBrep {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(SolidError::sm_err(OP_FIND_EDGE))
|
||||
Err(SolidError::blend_failed(
|
||||
OP_FIND_EDGE,
|
||||
"no fillet B-rep edge matches the fillet edge",
|
||||
))
|
||||
}
|
||||
|
||||
// ---- SmFilletGeom ---------------------------------------------------------------
|
||||
@@ -2480,7 +2489,7 @@ impl FilletBrep {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for an index past 1; [`SolidError::Upstream`]
|
||||
/// [`SolidError::InvalidInput`] for an index past 1; a blend failure
|
||||
/// (`SM_ERR_NULL_POINTER`) for a missing rail, primary edge use or mate (upstream
|
||||
/// asserts); [`SolidError::NotFound`] for dangling ids.
|
||||
pub fn rail_vertexuse(
|
||||
@@ -2495,14 +2504,16 @@ impl FilletBrep {
|
||||
let rail = self
|
||||
.geom(g)?
|
||||
.rail(rail_index)
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_RAIL_VU))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_RAIL_VU, "the rail"))?;
|
||||
let mut eu = primary_edgeuse(&self.brep, rail)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_RAIL_VU))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_RAIL_VU, "the rail's primary edge use"))?;
|
||||
if self.brep.edgeuse(eu)?.orientation == OrientType::Opposite {
|
||||
eu = mate(&self.brep, eu)?.ok_or_else(|| SolidError::null_pointer(OP_RAIL_VU))?;
|
||||
eu = mate(&self.brep, eu)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_RAIL_VU, "the edge use's mate"))?;
|
||||
}
|
||||
if end_index != 0 {
|
||||
eu = mate(&self.brep, eu)?.ok_or_else(|| SolidError::null_pointer(OP_RAIL_VU))?;
|
||||
eu = mate(&self.brep, eu)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_RAIL_VU, "the edge use's mate"))?;
|
||||
}
|
||||
Ok(self.brep.edgeuse(eu)?.vertexuse)
|
||||
}
|
||||
@@ -2513,7 +2524,7 @@ impl FilletBrep {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a missing rail;
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) for a missing rail;
|
||||
/// [`SolidError::Unported`] from a trim; [`SolidError::NotFound`] for dangling ids.
|
||||
/// With [`crate::kernel::UpstreamFixes::fillet_trim_status`] (or
|
||||
/// [`crate::kernel::UpstreamFixes::fillet_statuses_pass_on`]) enabled, every
|
||||
@@ -2530,7 +2541,9 @@ impl FilletBrep {
|
||||
let geom = self.geom(g)?;
|
||||
let mut rails: Vec<EdgeId> = Vec::new();
|
||||
for rail in geom.rails {
|
||||
rails.push(rail.ok_or_else(|| SolidError::null_pointer(OP_TRIM))?);
|
||||
rails.push(
|
||||
rail.ok_or_else(|| SolidError::blend_missing(OP_TRIM, "a rail of the geometry"))?,
|
||||
);
|
||||
}
|
||||
rails.extend_from_slice(&geom.other_rails1);
|
||||
rails.extend_from_slice(&geom.other_rails2);
|
||||
@@ -2723,12 +2736,12 @@ fn solution_uv<O: SolutionObject>(solutions: &SolutionArray<O>, index: usize) ->
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a face without a surface or one
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) for a face without a surface or one
|
||||
/// that is not a B-spline surface (the `SM_CAST_PTR` upstream); those of the
|
||||
/// evaluations.
|
||||
pub fn evaluate_mate(k: &Kernel, brep: &Brep, face: FaceId, uv: Point2) -> Result<Point3> {
|
||||
let surface =
|
||||
face2::surface(brep, face)?.ok_or_else(|| SolidError::null_pointer(OP_EVALUATE_MATE))?;
|
||||
let surface = face2::surface(brep, face)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EVALUATE_MATE, "the face's surface"))?;
|
||||
// SM_CAST_PTR(SmBSplineSurface, ..) and NER before the domain test: B-spline surfaces
|
||||
// and the kinds deriving from them (planes, surfaces of revolution, cones, spheres,
|
||||
// tori); the other kinds fail the cast
|
||||
@@ -2744,7 +2757,10 @@ pub fn evaluate_mate(k: &Kernel, brep: &Brep, face: FaceId, uv: Point2) -> Resul
|
||||
| SurfaceKind::ProceduralBlend(_)
|
||||
| SurfaceKind::BallPatch(_)
|
||||
) {
|
||||
return Err(SolidError::null_pointer(OP_EVALUATE_MATE));
|
||||
return Err(SolidError::blend_missing(
|
||||
OP_EVALUATE_MATE,
|
||||
"the face's B-spline surface",
|
||||
));
|
||||
}
|
||||
let natural_uv_domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
if natural_uv_domain.contains_point(uv, 0.0) {
|
||||
@@ -2762,7 +2778,10 @@ pub fn evaluate_mate(k: &Kernel, brep: &Brep, face: FaceId, uv: Point2) -> Resul
|
||||
SurfaceKind::ProceduralBlend(pb) => pb.net().evaluate_point_on_extended_surface(uv),
|
||||
// (the fork's SmBallPatch is an SmBSplineSurface too: its net)
|
||||
SurfaceKind::BallPatch(pb) => pb.net().evaluate_point_on_extended_surface(uv),
|
||||
_ => Err(SolidError::null_pointer(OP_EVALUATE_MATE)),
|
||||
_ => Err(SolidError::blend_missing(
|
||||
OP_EVALUATE_MATE,
|
||||
"the face's B-spline surface",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2774,7 +2793,7 @@ pub fn evaluate_mate(k: &Kernel, brep: &Brep, face: FaceId, uv: Point2) -> Resul
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR`) when no tolerance finds a solution; those of the
|
||||
/// A blend failure (`SM_ERR`) when no tolerance finds a solution; those of the
|
||||
/// point solve.
|
||||
///
|
||||
/// `handle` names `surface` in the solutions (upstream's solutions point at it).
|
||||
@@ -2791,7 +2810,7 @@ pub fn surface_drop_point<O: SolutionObject>(
|
||||
return Err(if e.is_unported_or_cancelled() {
|
||||
e
|
||||
} else {
|
||||
SolidError::null_pointer(OP_SURFACE_DROP_POINT)
|
||||
SolidError::blend_missing(OP_SURFACE_DROP_POINT, "the surface's cache")
|
||||
});
|
||||
}
|
||||
let domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
@@ -2820,7 +2839,10 @@ pub fn surface_drop_point<O: SolutionObject>(
|
||||
}
|
||||
|
||||
// Unable to drop point
|
||||
Err(SolidError::sm_err(OP_SURFACE_DROP_POINT))
|
||||
Err(SolidError::blend_failed(
|
||||
OP_SURFACE_DROP_POINT,
|
||||
"the point drops onto the surface at no tolerance",
|
||||
))
|
||||
}
|
||||
|
||||
/// Whether the surface is closed in `param` over `domain` (`IsClosed`).
|
||||
@@ -2988,7 +3010,10 @@ pub fn two_pnts_intersection(
|
||||
fn drop_or_bad<O: SolutionObject>(r: Result<SolutionArray<O>>) -> Result<SolutionArray<O>> {
|
||||
let solutions = r?;
|
||||
if solutions.is_empty() {
|
||||
return Err(upstream(OP_TWO_PNTS, SM_ERR_BAD_INTERSECTIONS));
|
||||
return Err(SolidError::blend(
|
||||
OP_TWO_PNTS,
|
||||
BlendFailure::BadIntersections("the start point drops onto the surfaces nowhere"),
|
||||
));
|
||||
}
|
||||
Ok(solutions)
|
||||
}
|
||||
@@ -2996,10 +3021,12 @@ fn drop_or_bad<O: SolutionObject>(r: Result<SolutionArray<O>>) -> Result<Solutio
|
||||
/// `DropVectors(uv, TRUE, TRUE, 1, &direction, &uv_dir)`.
|
||||
fn drop_vector(k: &Kernel, surface: &Surface, uv: Point2, direction: Vec3) -> Result<Vec2> {
|
||||
let dropped = drop_vectors_at(k, surface, uv, Side::Left, Side::Left, &[direction])?;
|
||||
dropped
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::sm_err(OP_TWO_PNTS))
|
||||
dropped.first().copied().ok_or_else(|| {
|
||||
SolidError::blend_failed(
|
||||
OP_TWO_PNTS,
|
||||
"the direction drops onto the surface as no vector",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `sm_FindStepoffUV`: the guess UVs for `PointOnPlaneSolve`: for each of the solver's
|
||||
@@ -3009,7 +3036,7 @@ fn drop_vector(k: &Kernel, surface: &Surface, uv: Point2, direction: Vec3) -> Re
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a solver without both edge
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) for a solver without both edge
|
||||
/// uses, an edge use without a face or a face without a surface; `SM_ERR` when a drop
|
||||
/// finds nothing; those of the step-off evaluation and point solve.
|
||||
pub fn find_stepoff_uv(
|
||||
@@ -3021,9 +3048,12 @@ pub fn find_stepoff_uv(
|
||||
) -> Result<[Point2; 2]> {
|
||||
let mut uvs = [Point2::new(0.0, 0.0); 2];
|
||||
for (slot, edgeuse) in uvs.iter_mut().zip(solver.solver.edgeuses) {
|
||||
let eu = edgeuse.ok_or_else(|| SolidError::null_pointer(OP_STEPOFF))?;
|
||||
let face = edgeuse_face(brep, eu)?.ok_or_else(|| SolidError::null_pointer(OP_STEPOFF))?;
|
||||
face2::surface(brep, face)?.ok_or_else(|| SolidError::null_pointer(OP_STEPOFF))?;
|
||||
let eu = edgeuse
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_STEPOFF, "the solver's edge use"))?;
|
||||
let face = edgeuse_face(brep, eu)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_STEPOFF, "the edge use's face"))?;
|
||||
face2::surface(brep, face)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_STEPOFF, "the face's surface"))?;
|
||||
// Step off the edge along the binormal direction
|
||||
let step = evaluate_binormal_step_off(
|
||||
k,
|
||||
@@ -3035,8 +3065,8 @@ pub fn find_stepoff_uv(
|
||||
)?;
|
||||
// The step-off needs the B-rep mutably, so the face's surface (checked above,
|
||||
// in upstream's order) is borrowed again after it.
|
||||
let surface =
|
||||
face2::surface(brep, face)?.ok_or_else(|| SolidError::null_pointer(OP_STEPOFF))?;
|
||||
let surface = face2::surface(brep, face)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_STEPOFF, "the face's surface"))?;
|
||||
let tol = solver.solver.approx_tol_3d;
|
||||
// Make the surface think that it is a standalone surface (point testing off).
|
||||
let solutions = surface.global_point_solve_point_test_off::<NoObject>(
|
||||
@@ -3050,7 +3080,10 @@ pub fn find_stepoff_uv(
|
||||
SolutionRequested::Single,
|
||||
)?;
|
||||
if solutions.is_empty() {
|
||||
return Err(SolidError::sm_err(OP_STEPOFF));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_STEPOFF,
|
||||
"the step-off point drops onto the face's surface nowhere",
|
||||
));
|
||||
}
|
||||
*slot = solution_uv(&solutions, 0);
|
||||
}
|
||||
@@ -3127,7 +3160,7 @@ fn point_3d_classify(
|
||||
/// [`SolidError::Unported`]: `SmFace::CreateUVTrimCurves` (`topology.face_p3`, through
|
||||
/// `SmFace::CalculateUVDomainFromUVTrimCurves`), the unported callees of
|
||||
/// `SmFace::Point3DClassify` ([`crate::query::face::p1::point_3d_classify`]);
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a face without a surface.
|
||||
/// a blend failure (`SM_ERR_NULL_POINTER`) for a face without a surface.
|
||||
pub fn classify_face_point(
|
||||
k: &Kernel,
|
||||
brep: &mut Brep,
|
||||
@@ -3225,8 +3258,8 @@ fn normalized_drop(
|
||||
pos: Point3,
|
||||
domain: &Extent2d,
|
||||
) -> Result<(bool, bool, Point2, f64)> {
|
||||
let surface =
|
||||
face2::surface(brep, face)?.ok_or_else(|| SolidError::null_pointer(OP_CLASSIFY))?;
|
||||
let surface = face2::surface(brep, face)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_CLASSIFY, "the face's surface"))?;
|
||||
let opts = DropPointOptions {
|
||||
guess_uv: None,
|
||||
operation: SolverOperation::Normalize,
|
||||
@@ -3421,7 +3454,7 @@ pub fn adjust_uv_on_seam(
|
||||
///
|
||||
/// Those of `SmFilletSolver::FindIndexOfRailOnFace` and `RailRailIntersect`
|
||||
/// ([`SolidError::Unported`] at its `SmTemporaryRadiusChange`,
|
||||
/// `blend.fillet_executive_p2`); [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a
|
||||
/// `blend.fillet_executive_p2`); a blend failure (`SM_ERR_NULL_POINTER`) for a
|
||||
/// rail index past the solver's two edge uses, a missing edge use or vertex use (all
|
||||
/// dereferenced unchecked upstream); `SM_ERR` when the rails do not intersect; those of
|
||||
/// the angle computations and [`BSplineCurve::create_canonical`].
|
||||
@@ -3445,7 +3478,7 @@ pub fn create_cubic_rail_rail_interpolant(
|
||||
.get(i)
|
||||
.copied()
|
||||
.flatten()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP))
|
||||
.ok_or_else(|| SolidError::blend_missing(OP, "the solver's edge use"))
|
||||
};
|
||||
// First, calculate the intersection of two rails
|
||||
let rail_index = fil_solver1
|
||||
@@ -3457,7 +3490,8 @@ pub fn create_cubic_rail_rail_interpolant(
|
||||
// Derive guess uv point here
|
||||
let mut eu = solver_edgeuse(fil_solver1.solver(), rail_index)?;
|
||||
let eu_vertex = |brep: &Brep, eu: EdgeuseId| -> Result<(VertexuseId, Option<VertexId>)> {
|
||||
let vu = eu1::vertexuse(brep, eu)?.ok_or_else(|| SolidError::null_pointer(OP))?;
|
||||
let vu = eu1::vertexuse(brep, eu)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP, "the edge use's vertex use"))?;
|
||||
Ok((vu, vertexuse_unit::vertex(brep, vu)?))
|
||||
};
|
||||
if eu_vertex(brep, eu)?.1 != Some(corner_vert) {
|
||||
@@ -3491,12 +3525,15 @@ pub fn create_cubic_rail_rail_interpolant(
|
||||
hold_end_radii,
|
||||
)?;
|
||||
if !found_intersection {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP,
|
||||
"the rails' cubic interpolant finds no intersection",
|
||||
));
|
||||
}
|
||||
let rail_srf = match rail_index {
|
||||
0 => SrfIndex::First,
|
||||
1 => SrfIndex::Second,
|
||||
_ => return Err(SolidError::null_pointer(OP)),
|
||||
_ => return Err(SolidError::blend_missing(OP, "the rail's surface index")),
|
||||
};
|
||||
// SmPoint2d to SmPoint3d: z = 0
|
||||
let uv_pnt = Point3::from(tsect_pnt1.uv_pos(rail_srf));
|
||||
@@ -3591,7 +3628,7 @@ pub fn cleanup_noise(uv: Point2) -> Point2 {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::Upstream`] (`SM_ERR_NULL_POINTER`) for a geometry without a fillet
|
||||
/// A blend failure (`SM_ERR_NULL_POINTER`) for a geometry without a fillet
|
||||
/// surface or rails, or, on the adjusting path, without a solver or a solver without
|
||||
/// the rail's offset surface (upstream dereferences them); `SM_ERR` when both surfaces
|
||||
/// need adjusting; those of the rail drop onto the solver's offset surface
|
||||
@@ -3654,14 +3691,12 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
|
||||
// check state - only high degree bspline surfaces can be examined
|
||||
{
|
||||
let surface1 = fb
|
||||
.geom(geom1)?
|
||||
.fillet_surface(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let surface2 = fb
|
||||
.geom(geom2)?
|
||||
.fillet_surface(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let surface1 = fb.geom(geom1)?.fillet_surface(None)?.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the first geometry's fillet surface")
|
||||
})?;
|
||||
let surface2 = fb.geom(geom2)?.fillet_surface(None)?.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the second geometry's fillet surface")
|
||||
})?;
|
||||
if surface1.degree_with_nurb(k, SurfParamType::U)? < 3
|
||||
|| surface2.degree_with_nurb(k, SurfParamType::U)? < 3
|
||||
|| !surface1.is_kind_of(BSplineSurface::TYPE)?
|
||||
@@ -3683,10 +3718,9 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
.zip(adjust_surf.iter_mut())
|
||||
.zip(orient.iter_mut())
|
||||
{
|
||||
let surface = fb
|
||||
.geom(geom)?
|
||||
.fillet_surface(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let surface = fb.geom(geom)?.fillet_surface(None)?.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the geometry's fillet surface")
|
||||
})?;
|
||||
let domain = surface.natural_uv_domain_with_nurb(k)?;
|
||||
let (uv1, uv2) = if at_start {
|
||||
(domain.evaluate(0.0, 0.0), domain.evaluate(0.0, 1.0))
|
||||
@@ -3719,7 +3753,10 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
}
|
||||
if adjust0 && adjust1 {
|
||||
// Can not adjust both surfaces
|
||||
return Err(SolidError::sm_err(OP_EXAMINE));
|
||||
return Err(SolidError::blend_failed(
|
||||
OP_EXAMINE,
|
||||
"both fillet surfaces need adjusting",
|
||||
));
|
||||
}
|
||||
|
||||
// Copy one row of control points from the 'good' fillet to the 'bad' fillet.
|
||||
@@ -3734,7 +3771,9 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
let from_surface = fb
|
||||
.geom_mut(from_geom)?
|
||||
.fillet_surface_mut(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the source geometry's fillet surface")
|
||||
})?;
|
||||
let Some(bs) = b_spline_net_mut(from_surface) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -3747,7 +3786,9 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
let to_surface = fb
|
||||
.geom_mut(to_geom)?
|
||||
.fillet_surface_mut(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the target geometry's fillet surface")
|
||||
})?;
|
||||
let Some(bs) = b_spline_net_mut(to_surface) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -3789,14 +3830,16 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
let adjust_solver = if adjust0 { solver1 } else { solver2 };
|
||||
let (surf_domain, rails) = {
|
||||
let g = fb.geom(adjust_fg)?;
|
||||
let surface = g
|
||||
.fillet_surface(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let surface = g.fillet_surface(None)?.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the adjusted geometry's fillet surface")
|
||||
})?;
|
||||
(surface.natural_uv_domain_with_nurb(k)?, g.rails)
|
||||
};
|
||||
|
||||
for (kk, rail) in rails.into_iter().enumerate() {
|
||||
let rail = rail.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let rail = rail.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "a rail of the adjusted geometry")
|
||||
})?;
|
||||
let iso_parameter = if kk == 1 {
|
||||
surf_domain.max().y
|
||||
} else {
|
||||
@@ -3805,10 +3848,9 @@ pub fn examine_n_fix_fillet_geoms_with_solvers(
|
||||
|
||||
// Produce iso-curve for rail
|
||||
let iso = {
|
||||
let surface = fb
|
||||
.geom(adjust_fg)?
|
||||
.fillet_surface(None)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let surface = fb.geom(adjust_fg)?.fillet_surface(None)?.ok_or_else(|| {
|
||||
SolidError::blend_missing(OP_EXAMINE, "the adjusted geometry's fillet surface")
|
||||
})?;
|
||||
surface.create_iso_parametric_curve(
|
||||
k,
|
||||
SurfParamType::V,
|
||||
@@ -3886,12 +3928,13 @@ fn drop_rail_onto_offset_surface(
|
||||
rail: EdgeId,
|
||||
dist_tol: f64,
|
||||
) -> Result<()> {
|
||||
let solver = adjust_solver.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let solver = adjust_solver
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EXAMINE, "the adjusted geometry's solver"))?;
|
||||
// SmFilletSolver::GetSurface(k); NER(pOffsetSurf). The offset distance is set to
|
||||
// 0 on a copy (upstream sets it on the solver's surface and restores it below).
|
||||
let mut offset_surf = solver
|
||||
.surface(rail_index)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EXAMINE, "the solver's offset surface"))?
|
||||
.surface
|
||||
.clone();
|
||||
let offset_surf = &mut offset_surf;
|
||||
@@ -3903,7 +3946,7 @@ fn drop_rail_onto_offset_surface(
|
||||
.edge(rail)?
|
||||
.curve
|
||||
.as_ref()
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EXAMINE, "the rail's curve"))?;
|
||||
let domain = offset_natural_uv_domain(offset_surf);
|
||||
let drop = offset_surface_drop_curve(
|
||||
k,
|
||||
@@ -3926,9 +3969,9 @@ fn drop_rail_onto_offset_surface(
|
||||
if let Ok([uv_curve]) = <[Curve; 1]>::try_from(uv_curves) {
|
||||
// upstream dereferences the primary edge use and its mate unchecked
|
||||
let prim_eu = primary_edgeuse(fb.brep(), rail)?
|
||||
.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
let mate_eu =
|
||||
mate(fb.brep(), prim_eu)?.ok_or_else(|| SolidError::null_pointer(OP_EXAMINE))?;
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EXAMINE, "the rail's primary edge use"))?;
|
||||
let mate_eu = mate(fb.brep(), prim_eu)?
|
||||
.ok_or_else(|| SolidError::blend_missing(OP_EXAMINE, "the primary edge use's mate"))?;
|
||||
// SetUVCurve(pUVCurve) with its defaults (bDeleteOldCurve = FALSE, bDbgWarnLeaks =
|
||||
// TRUE); upstream then deletes the curve it replaced
|
||||
fb.set_edgeuse_uv_curve(mate_eu, Some(uv_curve), false, true)?;
|
||||
@@ -4300,6 +4343,28 @@ mod tests {
|
||||
(fb, g, rails)
|
||||
}
|
||||
|
||||
/// #13790: a start point that drops onto the surfaces nowhere is a typed bad-intersections
|
||||
/// failure that keeps the `SM_ERR_BAD_INTERSECTIONS` status of the bare `Upstream` it
|
||||
/// replaced; any other status passes through untouched.
|
||||
#[test]
|
||||
fn an_empty_start_point_drop_is_a_typed_bad_intersections_failure() {
|
||||
let e = drop_or_bad::<NoObject>(Ok(SolutionArray::new())).unwrap_err();
|
||||
assert_eq!(
|
||||
e,
|
||||
SolidError::blend(
|
||||
OP_TWO_PNTS,
|
||||
BlendFailure::BadIntersections("the start point drops onto the surfaces nowhere"),
|
||||
)
|
||||
);
|
||||
assert_eq!(e.upstream_status(), Some(SM_ERR_BAD_INTERSECTIONS));
|
||||
assert_eq!(e.sm_status(), Some(SM_ERR_BAD_INTERSECTIONS));
|
||||
let other = SolidError::sm_err("x");
|
||||
assert_eq!(
|
||||
drop_or_bad::<NoObject>(Err(other.clone())).unwrap_err(),
|
||||
other
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_geom_default_drops_each_rail_curve_error_and_continues() {
|
||||
let k = Kernel::default();
|
||||
@@ -5037,10 +5102,12 @@ mod tests {
|
||||
.unwrap_err();
|
||||
// the real drop runs on rail 0 (a failed drop skips the rail); rail 1 has no
|
||||
// solver offset surface, which upstream dereferences without a check
|
||||
assert!(
|
||||
matches!(err, SolidError::Upstream { code: 1006, .. }),
|
||||
"{err:?}"
|
||||
// #13790: a typed blend failure with the status of the bare `Upstream` it replaced
|
||||
assert_eq!(
|
||||
err,
|
||||
SolidError::blend_missing(OP_EXAMINE, "the solver's offset surface")
|
||||
);
|
||||
assert_eq!(err.upstream_status(), Some(1006));
|
||||
// the offset distance was zeroed for the drop and restored
|
||||
assert_eq!(
|
||||
solver.solver.surfaces[0]
|
||||
@@ -5096,10 +5163,11 @@ mod tests {
|
||||
[None, None],
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, SolidError::Upstream { code: 1006, .. }),
|
||||
"{err:?}"
|
||||
assert_eq!(
|
||||
err,
|
||||
SolidError::blend_missing(OP_EXAMINE, "the adjusted geometry's solver")
|
||||
);
|
||||
assert_eq!(err.upstream_status(), Some(1006));
|
||||
// and with the good surface meeting the corner nothing is adjusted
|
||||
let (mut fb, [_, g1], [start_fv, end_fv], _, _) = corner_for_examine(&k);
|
||||
examine_n_fix_fillet_geoms(
|
||||
@@ -5194,7 +5262,7 @@ mod tests {
|
||||
identity,
|
||||
body: &body
|
||||
})),
|
||||
Err(SolidError::Upstream { .. })
|
||||
Err(e) if e == SolidError::blend_missing(OP_FILLET_SURFACE, "the fillet face's surface")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
+348
-202
File diff suppressed because it is too large
Load Diff
+2
-4
@@ -1853,10 +1853,8 @@ mod tests {
|
||||
use super::super::p1::{FilletEdgeuse, FilletVertexuse};
|
||||
|
||||
fn code(e: &SolidError) -> Option<i32> {
|
||||
match e {
|
||||
SolidError::Upstream { code, .. } => Some(*code),
|
||||
_ => None,
|
||||
}
|
||||
// a typed blend failure reads as the status of the bare `Upstream` (#13790)
|
||||
e.upstream_status()
|
||||
}
|
||||
|
||||
fn unit_of(e: &SolidError) -> Option<&'static str> {
|
||||
|
||||
+2
-7
@@ -2991,13 +2991,8 @@ mod tests {
|
||||
let e = fb
|
||||
.calc_fillet_int_2_fillets(&k, v[0], CornerLookup::default(), &[], &Brep::new())
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
e,
|
||||
SolidError::Upstream {
|
||||
code: crate::error::SM_ERR_NULL_POINTER,
|
||||
..
|
||||
}
|
||||
));
|
||||
// a typed blend failure keeps the status of the bare `Upstream` (#13790)
|
||||
assert_eq!(e.upstream_status(), Some(crate::error::SM_ERR_NULL_POINTER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+2
-7
@@ -3294,13 +3294,8 @@ mod tests {
|
||||
let e = fb
|
||||
.calc_fillet_int_fillet(&k, v[0], CornerLookup::default(), &[], &Brep::new())
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
e,
|
||||
SolidError::Upstream {
|
||||
code: crate::error::SM_ERR_NULL_POINTER,
|
||||
..
|
||||
}
|
||||
));
|
||||
// a typed blend failure keeps the status of the bare `Upstream` (#13790)
|
||||
assert_eq!(e.upstream_status(), Some(crate::error::SM_ERR_NULL_POINTER));
|
||||
// a corner in the table: no fillet/fillet edge at the vertex
|
||||
let corners = [FilletCorner::new(
|
||||
crate::blend::fillet_corner::p3::FilletCornerKind::FourByThree,
|
||||
|
||||
+327
-15
@@ -37,7 +37,7 @@
|
||||
//! | `PieceRows`, `tighten_level` (private) | — (not the fork's: a level's Taylor models taken only where they can change its verdict, #14405) |
|
||||
//! | [`TightPiece`], [`PieceInputs`], `resolve_pending`, `tighten_feet`, `gate` (private) | — (not the fork's: the feet's Taylor models, [`super::feet_tm`], taken only where they can change a level's verdict, #14405) |
|
||||
//! | `gn_try!` (private) | `SM_GN_TRY` |
|
||||
//! | [`EndContinuation`], [`nurbs_partials_over_box_ending`] | (not the fork's: #14284) |
|
||||
//! | [`EndContinuation`], [`ConeAxis`], [`nurbs_partials_over_box_ending`] | (not the fork's: #14284) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Out-params.** Every function returns its results. `sm_GnEnclose`'s `rbProved` FALSE
|
||||
@@ -50,7 +50,7 @@
|
||||
//! bounds, `gn_certify_span` the bound it has so far. Only a truncation past the series'
|
||||
//! fixed size (`n > SM_GN_MAXE`, which the fork leaves to its callers) is an `Err`.
|
||||
//! - **Bounds.** Every loop is the fork's, with its fixed counts (8 Krawczyk steps, 200
|
||||
//! contraction steps, levels 0..=6); there is no recursion.
|
||||
//! contraction steps, levels 0..=`SPAN_MAX_LEVEL` = 6); there is no recursion.
|
||||
//! - **Cancellation (#2136, #2371).** `gn_certify_span` polls the kernel's cancel token
|
||||
//! once per level, per piece's end station and per piece's bound; a cancel passes through
|
||||
//! `SM_GN_TRY` and the unsolved-piece fallback as `Cancelled` instead of proving
|
||||
@@ -121,8 +121,10 @@
|
||||
//! every feet's box past a face's knots, so a round ending flush with its support's end
|
||||
//! (a foot on the net's domain edge) proves nothing on its last pieces. Here a support whose
|
||||
//! kind is a line along one of its NURB directions continues that direction past the net:
|
||||
//! a plane in both, a cylinder (`Cone::is_cylinder`) or an
|
||||
//! extrusion along its line ([`EndContinuation::of`]), where the net is of degree 1 in it.
|
||||
//! a plane in both, a cone (a cylinder among them) or an extrusion along its line
|
||||
//! ([`EndContinuation::of`]), where the net is of degree 1 in it; a cone's continuation
|
||||
//! may not reach its apex, the cone's one point on its axis ([`ConeAxis`], from the cone's
|
||||
//! definition): the part's distance from the axis is proved above 0.
|
||||
//! The part of the box past the end takes the end span's polynomial, which for a degree-1
|
||||
//! direction is the face's edge jet continued, `P(u, v_e) + (v - v_e) P_v(u, v_e)`: the
|
||||
//! same polynomial on both sides of the end, so the feet's system is smooth to every order
|
||||
@@ -130,7 +132,7 @@
|
||||
//! the system C^1 for Krawczyk's mean-value form) needs. That this continuation is the
|
||||
//! support's definition (the plane's point, the cylinder's or extrusion's line at that
|
||||
//! parameter) is the kind's, and the closed-form tests show it. Every other support, a
|
||||
//! cone among them, and a direction of degree above 1 are refused past their end by name
|
||||
//! cone's part reaching its apex, and a direction of degree above 1 are refused past their end by name
|
||||
//! ([`crate::error::Reason::FootPastDomainEnd`]), the one refusal `SM_GN_TRY` passes on;
|
||||
//! any other failed evaluation still proves nothing.
|
||||
//! - **Indices.** Knots and nets are read by checked indices (a knot or a coordinate
|
||||
@@ -156,6 +158,9 @@ use crate::surface::surface::Surface;
|
||||
const OP: &str = "blend::fillet_section::certificate";
|
||||
/// The most Krawczyk steps that tighten a proved feet's box ([`gn_enclose_over`]).
|
||||
const TIGHTEN_STEPS: usize = 2;
|
||||
/// The finest level [`gn_certify_span`] cuts a span to: `2^SPAN_MAX_LEVEL` equal pieces
|
||||
/// (the fork's).
|
||||
pub(crate) const SPAN_MAX_LEVEL: u32 = 6;
|
||||
const OP_CERTIFY_SPAN: &str = "blend::fillet_section::certificate::gn_certify_span";
|
||||
|
||||
/// `SM_GN_TRY(a)`: a failed evaluation proves nothing, the enclosing function returns
|
||||
@@ -962,28 +967,59 @@ const FOOT_PAST_END: &str =
|
||||
/// (not the fork's, tangency-labs/solidcraft#14284), decided by the support's kind alone: a
|
||||
/// direction in which the kind is a line, so the net's degree-1 end span continued is the
|
||||
/// support there. See the module docs.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct EndContinuation {
|
||||
/// The NURB's u continues.
|
||||
pub u: bool,
|
||||
/// The NURB's v continues.
|
||||
pub v: bool,
|
||||
/// A cone's axis (not a cylinder's), which a continuation may not meet: its lines meet
|
||||
/// on it at the apex and run on onto the other nappe, not the face. `None` for a kind
|
||||
/// with no apex.
|
||||
pub apex: Option<ConeAxis>,
|
||||
}
|
||||
|
||||
/// A cone's axis from its definition: the point `origin` on it and its unit direction
|
||||
/// `axis`. The cone meets its axis only at its apex.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ConeAxis {
|
||||
/// A point of the axis (the cone's origin).
|
||||
pub origin: [f64; 3],
|
||||
/// The axis' unit direction.
|
||||
pub axis: [f64; 3],
|
||||
}
|
||||
|
||||
impl EndContinuation {
|
||||
/// The directions `surface`'s kind is a line in: a plane both; a cylinder its axial
|
||||
/// direction (the NURB's v, or u where its parameters are swapped); an extrusion its
|
||||
/// extrusion direction (likewise); none for every other kind, a cone included.
|
||||
/// The directions `surface`'s kind is a line in: a plane both; a cone (a cylinder among
|
||||
/// them) its generator direction (the NURB's v, or u where its parameters are swapped),
|
||||
/// with its apex where it has one; an extrusion its extrusion direction (likewise); none
|
||||
/// for every other kind.
|
||||
#[must_use]
|
||||
pub fn of(surface: &Surface) -> Self {
|
||||
use crate::surface::surface::p2::SurfaceKind;
|
||||
let along = |swapped: bool| Self {
|
||||
u: swapped,
|
||||
v: !swapped,
|
||||
apex: None,
|
||||
};
|
||||
match surface.kind() {
|
||||
SurfaceKind::Plane(_) => Self { u: true, v: true },
|
||||
SurfaceKind::Cone(c) if c.is_cylinder() => along(c.canonical().swap_uv),
|
||||
SurfaceKind::Plane(_) => Self {
|
||||
u: true,
|
||||
v: true,
|
||||
apex: None,
|
||||
},
|
||||
SurfaceKind::Cone(c) => {
|
||||
let canonical = c.canonical();
|
||||
let mut e = along(canonical.swap_uv);
|
||||
if !c.is_cylinder() {
|
||||
let (o, z) = (canonical.origin.origin(), canonical.origin.z_axis());
|
||||
e.apex = Some(ConeAxis {
|
||||
origin: [o.x, o.y, o.z],
|
||||
axis: [z.x, z.y, z.z],
|
||||
});
|
||||
}
|
||||
e
|
||||
}
|
||||
SurfaceKind::SurfOfExtrusion(e) => along(e.swap_uv()),
|
||||
_ => Self::default(),
|
||||
}
|
||||
@@ -1049,6 +1085,24 @@ pub fn nurbs_partials_over_box_ending(
|
||||
for &(a0, a1, ku) in &us {
|
||||
for &(b0, b1, kv) in &vs {
|
||||
let p = nurbs_partials_over_piece(cn, ku, kv, a0, a1, b0, b1, lk)?;
|
||||
// a part past an end of a cone may not reach its apex, the one point of the cone
|
||||
// on its axis: the part's points must be proved off the axis (the part is
|
||||
// connected and holds the end's points, so it then lies wholly on the face's
|
||||
// nappe)
|
||||
let past = a0 < u0 || a1 > u1 || b0 < v0 || b1 > v1;
|
||||
if let (true, Some(cone)) = (past, ends.apex) {
|
||||
let x = p.at(0, 0);
|
||||
let [ox, oy, oz] = cone.origin.map(Ivl::from);
|
||||
let [zx, zy, zz] = cone.axis.map(Ivl::from);
|
||||
let (dx, dy, dz) = (x.x - ox, x.y - oy, x.z - oz);
|
||||
let h = dx * zx + dy * zy + dz * zz;
|
||||
// the radial vector, off the axis: proved nonzero where one of its coordinates
|
||||
// is bounded away from 0 over the part
|
||||
let radial = [dx - h * zx, dy - h * zy, dz - h * zz];
|
||||
if !radial.iter().any(|c| c.mig() > 0.0) {
|
||||
return Err(refused());
|
||||
}
|
||||
}
|
||||
out = Some(match out {
|
||||
None => p,
|
||||
Some(h) => partials_hull(h, &p, lk),
|
||||
@@ -3751,7 +3805,7 @@ pub fn gn_certify_span_reusing(
|
||||
// feet's Taylor models are taken only while it is at most the target (#14405)
|
||||
let mut lb = 0.0_f64;
|
||||
// a halved span's pieces over this half: its level one coarser than theirs
|
||||
let mut start = reuse.start.min(6);
|
||||
let mut start = reuse.start.min(SPAN_MAX_LEVEL);
|
||||
let inherit = reuse.inherit;
|
||||
if let Some(pieces) = inherit.and_then(|l| covering(l, a.s, b.s)) {
|
||||
start = inherit.map_or(1, |l| l.level.max(1));
|
||||
@@ -3794,7 +3848,7 @@ pub fn gn_certify_span_reusing(
|
||||
}
|
||||
// the last level whose end stations were all solved, and those stations
|
||||
let mut solved_ends: Option<(u32, Vec<GnStation>)> = None;
|
||||
for level in start..=6_u32 {
|
||||
for level in start..=SPAN_MAX_LEVEL {
|
||||
k.check_cancelled(OP_CERTIFY_SPAN)?;
|
||||
let pieces = 1_usize << level;
|
||||
let coarser = match &solved_ends {
|
||||
@@ -4886,7 +4940,14 @@ mod tests {
|
||||
let o = Vec3::new(1.0, 2.0, 3.0);
|
||||
let fp = plane(o, Vec3::X, Vec3::Y);
|
||||
let ends = EndContinuation::of(&fp);
|
||||
assert_eq!(ends, EndContinuation { u: true, v: true });
|
||||
assert_eq!(
|
||||
ends,
|
||||
EndContinuation {
|
||||
u: true,
|
||||
v: true,
|
||||
apex: None
|
||||
}
|
||||
);
|
||||
let n = nurbs(&k, &fp);
|
||||
// on the domain: the same enclosure
|
||||
let inside =
|
||||
@@ -4942,7 +5003,14 @@ mod tests {
|
||||
cyl.make_nurb(&k).unwrap();
|
||||
let fc = Surface::from(cyl);
|
||||
let ends = EndContinuation::of(&fc);
|
||||
assert_eq!(ends, EndContinuation { u: false, v: true });
|
||||
assert_eq!(
|
||||
ends,
|
||||
EndContinuation {
|
||||
u: false,
|
||||
v: true,
|
||||
apex: None
|
||||
}
|
||||
);
|
||||
let n = nurbs(&k, &fc);
|
||||
assert_eq!(n.q, 1, "the cylinder's net is of degree 1 along its axis");
|
||||
let (u0, u1) = (knot(&n.u, n.p as isize), knot(&n.u, n.nu as isize));
|
||||
@@ -5409,6 +5477,250 @@ mod tests {
|
||||
assert!(median > 100.0, "{median}");
|
||||
}
|
||||
|
||||
/// A cone of `bot` radius at z = 0 to `top` at z = `height` about the z axis, a whole
|
||||
/// turn, its NURB made.
|
||||
fn upright_cone(k: &Kernel, bot: f64, top: f64, height: f64) -> Surface {
|
||||
let mut c = Cone::new_bounded(
|
||||
Vec3::ZERO,
|
||||
Vec3::X,
|
||||
Vec3::Y,
|
||||
bot,
|
||||
top,
|
||||
0.0,
|
||||
360.0,
|
||||
height,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
c.make_nurb(k).unwrap();
|
||||
Surface::from(c)
|
||||
}
|
||||
|
||||
/// #14284's cone: a cone continues past its net's ends along its generator only, where
|
||||
/// its feet's box holds the cone's own point `r(h) (cos t X + sin t Y) + h Z` past the
|
||||
/// top (`r(h)` its radius at height `h`, `t` the edge point's angle), and `S_v` holds the
|
||||
/// generator; past a u end it is refused, and so is a continued part that reaches the
|
||||
/// apex (the cone's one point on its axis).
|
||||
#[test]
|
||||
fn a_cone_s_box_continues_along_its_generator() {
|
||||
let k = Kernel::default();
|
||||
let (bot, top, height) = (5.0, 3.0, 4.0);
|
||||
let fc = upright_cone(&k, bot, top, height);
|
||||
let ends = EndContinuation::of(&fc);
|
||||
assert!(!ends.u && ends.v && ends.apex.is_some(), "{ends:?}");
|
||||
let n = nurbs(&k, &fc);
|
||||
assert_eq!(n.q, 1, "the cone's net is of degree 1 along its generator");
|
||||
let (u0, u1) = (knot(&n.u, n.p as isize), knot(&n.u, n.nu as isize));
|
||||
let (v0, v1) = (knot(&n.v, n.q as isize), knot(&n.v, n.nv as isize));
|
||||
// the cone's own point: height along v (an affine map of the net's v, read at its ends)
|
||||
let at = |u: f64, v: f64| fc.evaluate_point(&k, Vec2::new(u, v)).unwrap();
|
||||
for u in [u0 + 0.3 * (u1 - u0), u0 + 0.7 * (u1 - u0)] {
|
||||
let (h0, h1) = (at(u, v0).z, at(u, v1).z);
|
||||
let rate = (h1 - h0) / (v1 - v0);
|
||||
let edge = at(u, v1);
|
||||
let t = edge.y.atan2(edge.x);
|
||||
let radius = |h: f64| bot + (top - bot) * h / height;
|
||||
let cone = |h: f64| Vec3::new(radius(h) * t.cos(), radius(h) * t.sin(), h);
|
||||
let (va, vb) = (v1 - 0.5 * (v1 - v0), v1 + 0.2 * (v1 - v0));
|
||||
let e = nurbs_partials_over_box_ending(&n, ends, u, u, va, vb, 1, FEET_BOX_CONTINUITY)
|
||||
.unwrap();
|
||||
for v in [va, v1, 0.5 * (v1 + vb), vb] {
|
||||
let p = cone(h1 + (v - v1) * rate);
|
||||
assert!(
|
||||
holds(e.at(0, 0), p),
|
||||
"({u}, {v}): {p:?} not in {:?}",
|
||||
e.at(0, 0)
|
||||
);
|
||||
}
|
||||
let generator = cone(h1 + rate) - cone(h1);
|
||||
assert!(
|
||||
holds(e.at(0, 1), generator),
|
||||
"{:?} against {generator:?}",
|
||||
e.at(0, 1)
|
||||
);
|
||||
}
|
||||
// past a u end: no continuation there
|
||||
let past_u = nurbs_partials_over_box_ending(
|
||||
&n,
|
||||
ends,
|
||||
u1 - 0.1,
|
||||
u1 + 0.1,
|
||||
v0,
|
||||
v1,
|
||||
1,
|
||||
FEET_BOX_CONTINUITY,
|
||||
);
|
||||
assert!(
|
||||
matches!(past_u, Err(SolidError::Unsupported { reason, .. }) if reason == FOOT_PAST_END),
|
||||
"{past_u:?}"
|
||||
);
|
||||
// a cone whose apex lies just past its top: a continuation reaching it is refused
|
||||
let pointed = upright_cone(&k, 5.0, 0.05, 4.0);
|
||||
let pn = nurbs(&k, &pointed);
|
||||
let pe = EndContinuation::of(&pointed);
|
||||
let (w0, w1) = (knot(&pn.v, pn.q as isize), knot(&pn.v, pn.nv as isize));
|
||||
let pu = knot(&pn.u, pn.p as isize)
|
||||
+ 0.3 * (knot(&pn.u, pn.nu as isize) - knot(&pn.u, pn.p as isize));
|
||||
let short = nurbs_partials_over_box_ending(
|
||||
&pn,
|
||||
pe,
|
||||
pu,
|
||||
pu,
|
||||
w1 - 0.1 * (w1 - w0),
|
||||
w1 + 0.001 * (w1 - w0),
|
||||
1,
|
||||
FEET_BOX_CONTINUITY,
|
||||
);
|
||||
assert!(short.is_ok(), "short of the apex: {short:?}");
|
||||
let through = nurbs_partials_over_box_ending(
|
||||
&pn,
|
||||
pe,
|
||||
pu,
|
||||
pu,
|
||||
w1 - 0.1 * (w1 - w0),
|
||||
w1 + 0.1 * (w1 - w0),
|
||||
1,
|
||||
FEET_BOX_CONTINUITY,
|
||||
);
|
||||
assert!(
|
||||
matches!(through, Err(SolidError::Unsupported { reason, .. }) if reason == FOOT_PAST_END),
|
||||
"{through:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #14284's cone, a closed form: a ball of radius r between the plane z = 0 and an
|
||||
/// upright cone of semi-angle a (radius `bot` at z = 0) about the z axis, rolled round
|
||||
/// it. Its centre is the circle of radius `rho = bot + r (1 - sin a) / cos a` at height
|
||||
/// r; its feet are `(rho, theta, 0)` on the plane and `(rho - r cos a, theta, r (1 -
|
||||
/// sin a))` on the cone, at the height where the cone ends (its top), so the feet's box
|
||||
/// crosses the cone net's v end. With the cone continued (its generator), the feet are
|
||||
/// proved and hold the closed form; with the cone given as a plain B-spline (the same
|
||||
/// net, continuing nothing), the span is refused `FootPastDomainEnd`, nothing certified.
|
||||
/// (No procedural round of a cone ends where its cone ends: a cone standing on a plate
|
||||
/// rounds its root round a closed circle, so this is the end's case at the certificate.)
|
||||
#[test]
|
||||
fn a_ball_between_a_plane_and_a_cone_is_proved_at_the_cone_s_end() {
|
||||
let k = Kernel::default();
|
||||
let (bot, r) = (5.0, 2.0);
|
||||
let a = 30.0_f64.to_radians();
|
||||
let foot_h = r * (1.0 - a.sin());
|
||||
let top = bot - foot_h * a.tan();
|
||||
let fp = plane(Vec3::ZERO, Vec3::X, Vec3::Y);
|
||||
let fc = upright_cone(&k, bot, top, foot_h);
|
||||
let rho = bot + r * (1.0 - a.sin()) / a.cos();
|
||||
let frame = Axis2Placement::new(Vec3::new(0.0, 0.0, r), Vec3::X, Vec3::Y);
|
||||
let leader = BSplineCurve::create_circle_segment(
|
||||
3,
|
||||
&frame,
|
||||
rho,
|
||||
10.0,
|
||||
80.0,
|
||||
NurbCircleParam::Quadratic,
|
||||
)
|
||||
.unwrap();
|
||||
let ln = NurbsCurve::of(&leader).unwrap();
|
||||
let (np, nc) = (nurbs(&k, &fp), nurbs(&k, &fc));
|
||||
let faces = [&fp, &fc];
|
||||
let lpt = |s: f64| leader.evaluate_point(s).unwrap();
|
||||
let foot_c = |s: f64| {
|
||||
let l = lpt(s);
|
||||
let q = (rho - r * a.cos()) / rho;
|
||||
Vec3::new(l.x * q, l.y * q, foot_h)
|
||||
};
|
||||
// the sides: the plane's normal is +z, towards the ball; the cone's by its net
|
||||
let (cu, cv) = nearest_uv(&nc, foot_c(0.45));
|
||||
let pc = nurbs_partials(
|
||||
&nc,
|
||||
span(&nc.u, nc.p, nc.nu, cu),
|
||||
span(&nc.v, nc.q, nc.nv, cv),
|
||||
cu,
|
||||
cv,
|
||||
1,
|
||||
);
|
||||
let nrm = vec3(pc.at(1, 0)).cross(vec3(pc.at(0, 1)));
|
||||
let out = Vec3::new(foot_c(0.45).x, foot_c(0.45).y, 0.0);
|
||||
let side_c = if nrm.dot(out) > 0.0 { 1.0 } else { -1.0 };
|
||||
let (rr, side) = ([r, r], [1.0, side_c]);
|
||||
let st = |s: f64| {
|
||||
let l = lpt(s);
|
||||
let (u, v) = nearest_uv(&nc, foot_c(s));
|
||||
station(&k, faces, &leader, s, [l.x, l.y, u, v], rr, side)
|
||||
};
|
||||
// the span cut as `gn_certify_span` cuts it, `2^level` equal pieces, the level
|
||||
// raised to `SPAN_MAX_LEVEL` until every piece is proved. At level 0 (0.02 long)
|
||||
// Krawczyk's test fails (`gn_enclose` proves nothing) with the foot inside the cone
|
||||
// (a cone twice as tall) as at its end: the span's length, not the end. Past the end
|
||||
// the box is enclosed on the cone's own generator (`nurbs_partials_over_box_ending`,
|
||||
// which `partials_on` takes for a definition's box too), the end span continued.
|
||||
let (s0, s1) = (0.4, 0.42);
|
||||
let piece = |lo: f64, hi: f64| {
|
||||
let (sta, stb) = (st(lo), st(hi));
|
||||
SpanAt {
|
||||
s: Ivl::new(lo, hi),
|
||||
xa: core::array::from_fn(|c| sta.x[c].at(0, 0)),
|
||||
xb: core::array::from_fn(|c| stb.x[c].at(0, 0)),
|
||||
ku: [0; 2],
|
||||
kv: [0; 2],
|
||||
leader: None,
|
||||
piecewise: true,
|
||||
}
|
||||
};
|
||||
let enclose = |at: &SpanAt, ends| {
|
||||
gn_enclose_over(&k, [&np, &nc], &ln, at, rr, side, 0, 2, None, None, ends)
|
||||
};
|
||||
let continued = [EndContinuation::of(&fp), EndContinuation::of(&fc)];
|
||||
let free = Surface::from(as_b_spline(&k, &fc).unwrap().unwrap().into_owned());
|
||||
let plain = [EndContinuation::of(&fp), EndContinuation::of(&free)];
|
||||
let mut proved = None;
|
||||
for level in 0..=SPAN_MAX_LEVEL {
|
||||
let pieces = 1_u32 << level;
|
||||
let ends: Vec<f64> = (0..=pieces)
|
||||
.map(|i| s0 + f64::from(i) / f64::from(pieces) * (s1 - s0))
|
||||
.collect();
|
||||
let es: Option<Vec<_>> = ends
|
||||
.windows(2)
|
||||
.map(|w| {
|
||||
let at = piece(w[0], w[1]);
|
||||
enclose(&at, continued).unwrap().map(|e| (at, e))
|
||||
})
|
||||
.collect();
|
||||
if let Some(es) = es {
|
||||
proved = Some((level, es));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (level, es) = proved.expect("proved by SPAN_MAX_LEVEL");
|
||||
assert!(level >= 1, "the 0.02 span is proved at level 0");
|
||||
for (at, e) in &es {
|
||||
for s in [at.s.lo, at.s.hi] {
|
||||
let (l, c) = (lpt(s), foot_c(s));
|
||||
let fp_s = Vec3::new(l.x, l.y, 0.0);
|
||||
for (f, want) in [(0, fp_s), (1, c)] {
|
||||
let x = &e.f[f];
|
||||
let got = V3 {
|
||||
x: x.a[0].c[0][0],
|
||||
y: x.a[1].c[0][0],
|
||||
z: x.a[2].c[0][0],
|
||||
};
|
||||
assert!(
|
||||
holds(got, want),
|
||||
"level {level}: foot {f} at s {s}: {want:?} not in {got:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// the same net continuing nothing: refused by name on every piece
|
||||
let refused = enclose(at, plain);
|
||||
let Err(SolidError::Failed(f)) = &refused else {
|
||||
panic!("{refused:?}")
|
||||
};
|
||||
let crate::error::Reason::FootPastDomainEnd { support, .. } = &f.reason else {
|
||||
panic!("{f:?}")
|
||||
};
|
||||
assert_eq!(*support, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_curve_without_a_span_is_a_typed_failure() {
|
||||
let cn = NurbsCurve {
|
||||
|
||||
@@ -2310,16 +2310,13 @@ mod tests {
|
||||
);
|
||||
// SplitFilletGeom (wired to blend.fillet_geom_p1 on main) reaches the geometry's
|
||||
// rail vertex uses, which this fixture's rails do not have: an upstream error
|
||||
assert!(
|
||||
matches!(
|
||||
r,
|
||||
Err(SolidError::Upstream {
|
||||
operation: "blend::fillet_geom::p1::FilletBrep::rail_vertexuse",
|
||||
..
|
||||
})
|
||||
),
|
||||
"{r:?}"
|
||||
let e = r.unwrap_err();
|
||||
assert_eq!(
|
||||
e.operation(),
|
||||
"blend::fillet_geom::p1::FilletBrep::rail_vertexuse",
|
||||
"{e:?}"
|
||||
);
|
||||
assert!(e.upstream_status().is_some(), "{e:?}");
|
||||
assert!(fb.geom(g).unwrap().fillet_surface.is_some());
|
||||
}
|
||||
|
||||
|
||||
+10
-14
@@ -886,13 +886,11 @@ fn public_split_helper_rejects_a_missing_infinite_region_after_partial_edits() {
|
||||
assert!(f.fb.brep().infinite_region().is_some());
|
||||
f.fb.brep_mut().set_infinite_region_record(None);
|
||||
let outcome = super::super::super::p1::split_rail(&f.k, &mut f.fb, &mut f.owners, id, rail);
|
||||
assert!(matches!(
|
||||
outcome,
|
||||
Err(SolidError::Upstream {
|
||||
code: crate::error::SM_ERR_NULL_POINTER,
|
||||
..
|
||||
})
|
||||
));
|
||||
// a typed blend failure keeps the status of the bare `Upstream` (#13790)
|
||||
assert_eq!(
|
||||
outcome.unwrap_err().upstream_status(),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER)
|
||||
);
|
||||
assert_ne!(f.context().end(rail).unwrap(), old_end);
|
||||
assert!(f.fb.brep().vertex(old_end).unwrap().vertexuses.is_empty());
|
||||
assert!(f.owners.notifications.is_empty());
|
||||
@@ -1063,13 +1061,11 @@ fn rollover_corner_vertex_delegates_to_the_available_geom_port() {
|
||||
let start = f.context().start(rail).unwrap();
|
||||
let r = Missing.corner_vertex(&mut f.context(), start, id);
|
||||
// the fixture's corner table is empty: the vertex's corner is not in it
|
||||
assert!(matches!(
|
||||
r,
|
||||
Err(SolidError::Upstream {
|
||||
code: crate::error::SM_ERR_NULL_POINTER,
|
||||
..
|
||||
})
|
||||
));
|
||||
// a typed blend failure keeps the status of the bare `Upstream` (#13790)
|
||||
assert_eq!(
|
||||
r.unwrap_err().upstream_status(),
|
||||
Some(crate::error::SM_ERR_NULL_POINTER)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+2
@@ -17,6 +17,7 @@
|
||||
//! unchanged.
|
||||
|
||||
pub mod arc;
|
||||
pub mod arc_of;
|
||||
pub mod composite_curve;
|
||||
pub mod conic;
|
||||
pub mod crv_in_volume;
|
||||
@@ -35,6 +36,7 @@ pub(crate) mod native_affine;
|
||||
pub mod offset_curve;
|
||||
pub mod parabola;
|
||||
pub mod path;
|
||||
pub mod plane_of;
|
||||
pub mod projected_curve;
|
||||
pub mod step_edge;
|
||||
pub mod tangent_field;
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
//! A record circle over an interval of its angle as an arc's definition ([`ArcDefinition`]):
|
||||
//! the circle `origin + radius (cos t x + sin t y)` from `t = start` to `t = end` is the arc about
|
||||
//! `origin` of `radius`, counter-clockwise about `x × y` from `x`, over the same angles. The
|
||||
//! definition is the record's numbers, the axis their one product: the arc's circle
|
||||
//! ([`ArcDefinition::placement`]) is the record's, within [`crate::curve::arc::frame_error`].
|
||||
//!
|
||||
//! Not upstream: the modelling layer's record curves taken as sweep arcs, kept in the kernel so
|
||||
//! the layer computes no geometry (docs/layers.md §1). Deviations: none (no upstream
|
||||
//! counterpart). No recursion.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use crate::curve::arc::{frame_error, ArcDefinition, MIN_ARC};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::eval::Frame;
|
||||
use crate::geom::certified::{difference, distance, dot, Bounded};
|
||||
use crate::geom::vector3d::Vec3;
|
||||
|
||||
const OP: &str = "curve::arc_of::arc_of_circle";
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vec3 {
|
||||
Vec3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
/// The arc of the record circle on `frame` of `radius` from angle `interval[0]` to
|
||||
/// `interval[1]` (radians from the frame's x axis, counter-clockwise about `x × y`): centre the
|
||||
/// frame's origin, reference its x axis, axis `x × y`, the same radius and angles.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for numbers not finite, a radius below [`MIN_ARC`], an interval that does not
|
||||
/// increase by at least [`MIN_ARC`] or turns more than once round, or axes not proved of unit
|
||||
/// length and square to each other to the arc's own frame error ([`frame_error`]): a frame
|
||||
/// farther off would make the arc's circle (its placement made orthonormal) another circle
|
||||
/// than the record's, by that much times the radius.
|
||||
pub fn arc_of_circle(frame: &Frame, radius: f64, interval: [f64; 2]) -> Result<ArcDefinition> {
|
||||
let bad = |reason: &'static str| SolidError::invalid_input(OP, reason);
|
||||
let [start, end] = interval;
|
||||
let finite = [frame.origin, frame.x_axis, frame.y_axis]
|
||||
.iter()
|
||||
.flatten()
|
||||
.chain(&[radius, start, end])
|
||||
.all(|x| x.is_finite());
|
||||
if !finite {
|
||||
return Err(bad("not finite"));
|
||||
}
|
||||
if radius < MIN_ARC {
|
||||
return Err(bad("a radius below the smallest arc's"));
|
||||
}
|
||||
if end - start < MIN_ARC {
|
||||
return Err(bad("an interval that does not increase"));
|
||||
}
|
||||
if end - start > TAU {
|
||||
return Err(bad("an interval past a full turn"));
|
||||
}
|
||||
let (x, y) = (v3(frame.x_axis), v3(frame.y_axis));
|
||||
// the record's frame: unit axes square to each other, each proved to the arc's own frame
|
||||
// error, so the arc's orthonormal placement is the record's circle to within the error its
|
||||
// placement has anyway, at any radius (a relative bound)
|
||||
let within = frame_error();
|
||||
let unit_long = |v: Vec3| {
|
||||
let length = distance(Vec3::ZERO, v);
|
||||
let off = difference(length.value, 1.0);
|
||||
Bounded {
|
||||
value: off.value,
|
||||
bound: off.bound + length.bound,
|
||||
}
|
||||
.at_most(within)
|
||||
.is_yes()
|
||||
};
|
||||
if !unit_long(x) || !unit_long(y) {
|
||||
return Err(bad("axes not of unit length"));
|
||||
}
|
||||
let across = dot(x, y, 0.0, 0.0);
|
||||
let across = Bounded {
|
||||
value: across.value.abs(),
|
||||
..across
|
||||
};
|
||||
if !across.at_most(within).is_yes() {
|
||||
return Err(bad("axes not square to each other"));
|
||||
}
|
||||
let arc = ArcDefinition {
|
||||
centre: v3(frame.origin),
|
||||
axis: x.cross(y),
|
||||
reference: x,
|
||||
radius,
|
||||
start,
|
||||
end,
|
||||
};
|
||||
// the placement's own checks: an axis with a length, the reference square to it
|
||||
arc.placement()?;
|
||||
Ok(arc)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn frame(x: [f64; 3], y: [f64; 3]) -> Frame {
|
||||
Frame {
|
||||
origin: [1.0, 2.0, 3.0],
|
||||
x_axis: x,
|
||||
y_axis: y,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_circle_is_the_arc_about_x_cross_y_from_x() {
|
||||
let f = frame([0.0, 1.0, 0.0], [0.0, 0.0, 1.0]);
|
||||
let arc = arc_of_circle(&f, 5.0, [0.25, 2.0]).unwrap();
|
||||
assert_eq!(arc.centre, Vec3::new(1.0, 2.0, 3.0));
|
||||
assert_eq!(arc.axis, Vec3::new(1.0, 0.0, 0.0));
|
||||
assert_eq!(arc.reference, Vec3::new(0.0, 1.0, 0.0));
|
||||
assert_eq!((arc.radius, arc.start, arc.end), (5.0, 0.25, 2.0));
|
||||
// the arc's points are the record circle's: origin + r (cos t x + sin t y)
|
||||
for t in [0.25, 1.0, 2.0] {
|
||||
let p = arc.point_at(t).unwrap();
|
||||
let want = Vec3::new(1.0, 2.0 + 5.0 * t.cos(), 3.0 + 5.0 * t.sin());
|
||||
assert!((p - want).length() < 1e-12, "{p:?} at {t}");
|
||||
}
|
||||
// a whole turn is an arc
|
||||
assert!(arc_of_circle(&f, 5.0, [0.0, TAU]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_circles_and_intervals_are_refused() {
|
||||
let f = frame([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||
let refused = |f: &Frame, r: f64, i: [f64; 2]| {
|
||||
matches!(arc_of_circle(f, r, i), Err(SolidError::InvalidInput { .. }))
|
||||
};
|
||||
assert!(refused(&f, 0.0, [0.0, 1.0]));
|
||||
assert!(refused(&f, f64::NAN, [0.0, 1.0]));
|
||||
assert!(refused(&f, 1.0, [1.0, 1.0]));
|
||||
assert!(refused(&f, 1.0, [1.0, 0.0]));
|
||||
assert!(refused(&f, 1.0, [0.0, TAU + 1e-9]));
|
||||
assert!(refused(&f, 1.0, [0.0, f64::INFINITY]));
|
||||
// parallel axes: no axis
|
||||
assert!(refused(
|
||||
&frame([1.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
|
||||
1.0,
|
||||
[0.0, 1.0]
|
||||
));
|
||||
// axes not square, or not unit
|
||||
let w = std::f64::consts::FRAC_1_SQRT_2;
|
||||
assert!(refused(
|
||||
&frame([1.0, 0.0, 0.0], [w, w, 0.0]),
|
||||
1.0,
|
||||
[0.0, 1.0]
|
||||
));
|
||||
assert!(refused(
|
||||
&frame([2.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
|
||||
1.0,
|
||||
[0.0, 1.0]
|
||||
));
|
||||
// nearly square (within 1e-9) is not the record's circle at a large radius: at 1e6 its
|
||||
// point at a quarter turn is 5e-4 off the arc's
|
||||
assert!(refused(
|
||||
&frame([1.0, 0.0, 0.0], [5e-10, 1.0, 0.0]),
|
||||
1e6,
|
||||
[0.0, 2.0]
|
||||
));
|
||||
}
|
||||
}
|
||||
+28
-3
@@ -16,11 +16,13 @@
|
||||
//! | [`CompositeCurve::create_offsets_of_many_curves`], [`OffsetParams`], [`TrimmedOffsets`], [`OffsetPiece`] | `CreateOffsetsOfManyCurves` (static) |
|
||||
//! | [`CompositeCurve::intersect_and_trim_offsets`] | `IntersectAndTrimOffsets` (static) |
|
||||
//! | [`CompositeCurve::create_trimmed_offsets`], [`OffsetDirection`] | `CreateTrimmedOffsets`, `SmOffsetDirectionType` |
|
||||
//! | [`CompositeCurve::make_composite_nurb`] | `MakeCompositeNurb` |
|
||||
//! | [`CompositeCurve::make_composite_nurb`], [`CompositeCurve::make_composite_nurb_with_breaks`] | `MakeCompositeNurb` |
|
||||
//! | [`CompositeCurve::read_from_db`] | `SmCompositeCurve::ReadFromDB` (static) |
|
||||
//! | [`CompositeCurve::write_to_db`] | `SmCompositeCurve::WriteToDB` |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - **Join breaks** (not upstream). [`CompositeCurve::make_composite_nurb_with_breaks`]
|
||||
//! also returns each segment's span in the joined curve (tangency-labs/solidcore#3139).
|
||||
//! - **Attributes.** A [`Curve`] carries no attribute list, so the offsetters pass each
|
||||
//! curve with its offset map attribute as an [`OffsetPiece`]. Copies and trims keep
|
||||
//! the attribute (upstream's `SM_AB_COPY` behaviour copies it with `MakeCopy`), and
|
||||
@@ -858,6 +860,22 @@ impl CompositeCurve {
|
||||
/// Those of the join; [`SolidError::InvalidInput`] for a segment that is not an
|
||||
/// `SmBSplineCurve` (upstream joins a `NULL` curve).
|
||||
pub fn make_composite_nurb(&mut self) -> Result<BSplineCurve> {
|
||||
self.composite_nurb(false).map(|(joined, _)| joined)
|
||||
}
|
||||
|
||||
/// [`CompositeCurve::make_composite_nurb`], also returning where each segment lies
|
||||
/// in the joined curve ([`BSplineCurve::create_by_joining_with_breaks`]: segment `i`
|
||||
/// spans `[breaks[i], breaks[i + 1]]`, each value one of the curve's knots).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`CompositeCurve::make_composite_nurb`].
|
||||
pub fn make_composite_nurb_with_breaks(&mut self) -> Result<(BSplineCurve, Vec<f64>)> {
|
||||
self.composite_nurb(true)
|
||||
}
|
||||
|
||||
/// `MakeCompositeNurb`, with the join's breaks when asked (empty otherwise).
|
||||
fn composite_nurb(&mut self, with_breaks: bool) -> Result<(BSplineCurve, Vec<f64>)> {
|
||||
// get arrays of composite curve segments and their senses
|
||||
let mut bsplines = Vec::with_capacity(self.segments.len());
|
||||
let mut senses = Vec::with_capacity(self.segments.len());
|
||||
@@ -880,7 +898,14 @@ impl CompositeCurve {
|
||||
}
|
||||
|
||||
// create the joined curve
|
||||
let joined = BSplineCurve::create_by_joining(&mut bsplines, Some(&senses))?;
|
||||
let (joined, breaks) = if with_breaks {
|
||||
BSplineCurve::create_by_joining_with_breaks(&mut bsplines, Some(&senses))?
|
||||
} else {
|
||||
(
|
||||
BSplineCurve::create_by_joining(&mut bsplines, Some(&senses))?,
|
||||
Vec::new(),
|
||||
)
|
||||
};
|
||||
|
||||
// the join raised the B-spline segments in place
|
||||
for (s, b) in self.segments.iter_mut().zip(bsplines) {
|
||||
@@ -888,7 +913,7 @@ impl CompositeCurve {
|
||||
*x = b;
|
||||
}
|
||||
}
|
||||
Ok(joined)
|
||||
Ok((joined, breaks))
|
||||
}
|
||||
|
||||
/// `CreateTrimmedOffsets`: [`Self::create_offsets_of_many_curves`] of this curve
|
||||
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
//! The plane a set of curves lies in, within a tolerance, certified.
|
||||
//!
|
||||
//! Upstream: none (not upstream; solidcore-native).
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |---------------|----------|
|
||||
//! | [`plane_of`] | none |
|
||||
//! | [`PlaneOf`] | none |
|
||||
//!
|
||||
//! Each curve lies in the convex hull of points the record gives it: a line segment of its two
|
||||
//! points, a polyline and a spline of their control points, a rational spline of its Euclidean
|
||||
//! control points (weights positive), a circle or an ellipse of the four corners of the
|
||||
//! parallelogram `origin ± r_x x ± r_y y` round it (any arc of it lies inside that too).
|
||||
//! A slab is convex, so every point of every curve is within the tolerance of a plane when
|
||||
//! every one of those points is. The plane is the one `surface::implicit::plane_of_net` takes
|
||||
//! for a net: through the first point, square to the two chords from there that span the most
|
||||
//! (the farthest point, then the one farthest off that chord), so its normal is as well
|
||||
//! conditioned as the points allow.
|
||||
//!
|
||||
//! Rounding (docs/definitions-and-certificates.md §4.4): a corner of a conic carries
|
||||
//! `γ_3 (|o| + r_x |x| + r_y |y|)` per coordinate, a Euclidean point of a rational spline
|
||||
//! `γ_1` of itself; a point off by `e` per coordinate is at most `2 e` (`√3 e`) farther from the
|
||||
//! plane. The distance `|(q - c) . n| / |n|` is computed with the normal made unit: the
|
||||
//! difference rounds by `γ_1` and the dot by `γ_3` of `|q - c|` per component (`2 (γ_1 + γ_3)
|
||||
//! |q - c|` in all), and the unit normal's length is 1 within `γ_8`.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). A line is read as its segment from `start` to
|
||||
//! `end`. No recursion.
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::eval::Curve;
|
||||
use crate::math::{Point3, Vec3};
|
||||
use crate::nurbs::math::gamma;
|
||||
|
||||
const OP: &str = "curve::plane_of::plane_of";
|
||||
|
||||
/// A plane the curves lie in: a point of it, its unit normal, and the certified bound of the
|
||||
/// largest distance of any point of the curves from it (at most the tolerance asked).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct PlaneOf {
|
||||
/// A point of the plane: the curves' first point.
|
||||
pub origin: Point3,
|
||||
/// The plane's unit normal.
|
||||
pub normal: Vec3,
|
||||
/// No point of the curves is farther from the plane than this.
|
||||
pub deviation: f64,
|
||||
}
|
||||
|
||||
/// The plane every point of `curves` lies within `tol` of, certified (module docs), or `None`
|
||||
/// when no such plane is proved: the curves are not planar within `tol`, or they span no plane
|
||||
/// (all within `tol` of one point or of one line).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for no curves, a tolerance not finite and positive, numbers not finite, or
|
||||
/// a rational spline with a weight not positive; `Unsupported` for a traced intersection
|
||||
/// curve; a query failure for curves so large their chords or normal overflow.
|
||||
pub fn plane_of(curves: &[Curve], tol: f64) -> Result<Option<PlaneOf>> {
|
||||
if curves.is_empty() {
|
||||
return Err(SolidError::invalid_input(OP, "no curves"));
|
||||
}
|
||||
if !(tol.is_finite() && tol > 0.0) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"a tolerance not finite and positive",
|
||||
));
|
||||
}
|
||||
let mut points: Vec<(Point3, f64)> = Vec::new();
|
||||
for c in curves {
|
||||
hull(c, &mut points)?;
|
||||
}
|
||||
if !points
|
||||
.iter()
|
||||
.all(|(p, e)| p.x.is_finite() && p.y.is_finite() && p.z.is_finite() && e.is_finite())
|
||||
{
|
||||
return Err(SolidError::invalid_input(OP, "numbers not finite"));
|
||||
}
|
||||
let Some(&(c0, e0)) = points.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let far = |a: &&(Point3, f64), b: &&(Point3, f64)| {
|
||||
(a.0 - c0).length().total_cmp(&(b.0 - c0).length())
|
||||
};
|
||||
let Some(&(along, _)) = points.iter().max_by(far) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let chord = along - c0;
|
||||
let reach = chord.length();
|
||||
if reach <= tol {
|
||||
return Ok(None);
|
||||
}
|
||||
let off = |a: &&(Point3, f64), b: &&(Point3, f64)| {
|
||||
chord
|
||||
.cross(a.0 - c0)
|
||||
.length()
|
||||
.total_cmp(&chord.cross(b.0 - c0).length())
|
||||
};
|
||||
let Some(&(across, _)) = points.iter().max_by(off) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let cross = chord.cross(across - c0);
|
||||
// every point within tol of the line along the chord: no plane is singled out
|
||||
if cross.length() / reach <= tol {
|
||||
return Ok(None);
|
||||
}
|
||||
let Ok(normal) = cross.unitize() else {
|
||||
return Ok(None);
|
||||
};
|
||||
// A size past the floating range (a chord or a cross product overflowing) leaves no
|
||||
// normal to bound with: refused, never read as a plane. `f64::max` would drop a NaN.
|
||||
let overflow = || SolidError::query_failed(OP, "the curves' size overflows the bound");
|
||||
if !(reach.is_finite() && normal.x.is_finite() && normal.y.is_finite() && normal.z.is_finite())
|
||||
{
|
||||
return Err(overflow());
|
||||
}
|
||||
let mut deviation = 0.0_f64;
|
||||
for &(q, e) in &points {
|
||||
let w = q - c0;
|
||||
let d = w.dot(normal).abs();
|
||||
let bound = (d * (1.0 + gamma(8))
|
||||
+ 2.0 * (gamma(1) + gamma(3)) * w.length() * (1.0 + gamma(5))
|
||||
+ 2.0 * (e + e0))
|
||||
* (1.0 + gamma(2));
|
||||
if !bound.is_finite() {
|
||||
return Err(overflow());
|
||||
}
|
||||
deviation = deviation.max(bound);
|
||||
}
|
||||
Ok((deviation <= tol).then_some(PlaneOf {
|
||||
origin: c0,
|
||||
normal,
|
||||
deviation,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Pushes the points whose convex hull holds `c`, each with the error of its coordinates.
|
||||
fn hull(c: &Curve, out: &mut Vec<(Point3, f64)>) -> Result<()> {
|
||||
let p3 = |p: &[f64; 3]| Point3::new(p[0], p[1], p[2]);
|
||||
match c {
|
||||
Curve::Line { start, end } => {
|
||||
out.push((p3(start), 0.0));
|
||||
out.push((p3(end), 0.0));
|
||||
}
|
||||
Curve::Polyline { points } | Curve::Spline { points, .. } => {
|
||||
out.extend(points.iter().map(|p| (p3(p), 0.0)));
|
||||
}
|
||||
Curve::RationalSpline { homogeneous, .. } => {
|
||||
for &[x, y, z, w] in homogeneous {
|
||||
if !(w.is_finite() && w > 0.0) {
|
||||
return Err(SolidError::invalid_input(OP, "a weight not positive"));
|
||||
}
|
||||
let q = Point3::new(x / w, y / w, z / w);
|
||||
let e = gamma(1) * q.x.abs().max(q.y.abs()).max(q.z.abs());
|
||||
out.push((q, e));
|
||||
}
|
||||
}
|
||||
Curve::Circle { frame, radius } => corners(frame, *radius, *radius, out),
|
||||
Curve::Ellipse { frame, radii } => corners(frame, radii[0], radii[1], out),
|
||||
Curve::SurfaceIntersection { .. } => {
|
||||
return Err(SolidError::Unsupported {
|
||||
operation: OP,
|
||||
reason: "a traced intersection curve",
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The four corners of the parallelogram `origin ± rx x ± ry y` round a conic of the frame,
|
||||
/// each with the rounding of computing it.
|
||||
fn corners(frame: &crate::eval::Frame, rx: f64, ry: f64, out: &mut Vec<(Point3, f64)>) {
|
||||
let v = |p: [f64; 3]| Vec3::new(p[0], p[1], p[2]);
|
||||
let (o, x, y) = (v(frame.origin), v(frame.x_axis), v(frame.y_axis));
|
||||
let (rx, ry) = (rx.abs(), ry.abs());
|
||||
let size = |a: Vec3| a.x.abs().max(a.y.abs()).max(a.z.abs());
|
||||
let e = gamma(3) * (size(o) + rx * size(x) + ry * size(y));
|
||||
for (sx, sy) in [(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)] {
|
||||
out.push((o + x * (sx * rx) + y * (sy * ry), e));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::eval::Frame;
|
||||
|
||||
fn spline(points: &[[f64; 3]]) -> Curve {
|
||||
let n = points.len();
|
||||
let mut knots = vec![0.0; 4];
|
||||
knots.extend((1..n - 3).map(|i| i as f64 / (n - 3) as f64));
|
||||
knots.extend([1.0; 4]);
|
||||
Curve::Spline {
|
||||
knots,
|
||||
points: points.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_spline_and_a_line_in_one_plane_give_it() {
|
||||
let wave = spline(&[
|
||||
[0.0, 0.0, 5.0],
|
||||
[10.0, 4.0, 5.0],
|
||||
[20.0, -3.0, 5.0],
|
||||
[30.0, 2.0, 5.0],
|
||||
]);
|
||||
let line = Curve::Line {
|
||||
start: [30.0, 2.0, 5.0],
|
||||
end: [0.0, 0.0, 5.0],
|
||||
};
|
||||
let plane = plane_of(&[wave, line], 1e-6).unwrap().unwrap();
|
||||
assert_eq!(plane.origin, Point3::new(0.0, 0.0, 5.0));
|
||||
assert!(plane.normal.z.abs() > 1.0 - 1e-15, "{:?}", plane.normal);
|
||||
assert!(plane.deviation < 1e-12, "{}", plane.deviation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_twisted_spline_is_no_plane_unless_the_tolerance_takes_it() {
|
||||
let twisted = spline(&[
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
[0.0, 10.0, 1.0],
|
||||
]);
|
||||
assert_eq!(
|
||||
plane_of(std::slice::from_ref(&twisted), 1e-6).unwrap(),
|
||||
None
|
||||
);
|
||||
let slightly = spline(&[
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
[0.0, 10.0, 1e-8],
|
||||
]);
|
||||
let plane = plane_of(&[slightly], 1e-6).unwrap().unwrap();
|
||||
assert!(plane.deviation <= 1e-6 && plane.deviation >= 0.0);
|
||||
assert!(plane_of(&[twisted], 2.0).unwrap().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collinear_curves_or_one_point_span_no_plane() {
|
||||
let a = Curve::Line {
|
||||
start: [0.0, 0.0, 0.0],
|
||||
end: [1.0, 1.0, 1.0],
|
||||
};
|
||||
let b = Curve::Polyline {
|
||||
points: vec![[2.0, 2.0, 2.0], [5.0, 5.0, 5.0]],
|
||||
};
|
||||
assert_eq!(plane_of(&[a, b], 1e-6).unwrap(), None);
|
||||
let dot = Curve::Line {
|
||||
start: [1.0, 2.0, 3.0],
|
||||
end: [1.0, 2.0, 3.0],
|
||||
};
|
||||
assert_eq!(plane_of(&[dot], 1e-6).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_circle_lies_in_its_frames_plane() {
|
||||
let s = std::f64::consts::FRAC_1_SQRT_2;
|
||||
let frame = Frame {
|
||||
origin: [1.0, 2.0, 3.0],
|
||||
x_axis: [s, s, 0.0],
|
||||
y_axis: [-0.5, 0.5, s],
|
||||
};
|
||||
let circle = Curve::Circle { frame, radius: 4.0 };
|
||||
let plane = plane_of(&[circle], 1e-9).unwrap().unwrap();
|
||||
// the frame's z: x cross y
|
||||
let z = Vec3::new(s * s, -s * s, s * 0.5 + s * 0.5);
|
||||
let along = plane.normal.dot(z).abs() / z.length();
|
||||
assert!((along - 1.0).abs() < 1e-12, "{:?}", plane.normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curves_too_large_for_the_bound_are_refused_not_read_as_a_plane() {
|
||||
// finite points whose chords' cross product overflows: no finite normal
|
||||
let huge = Curve::Polyline {
|
||||
points: vec![[0.0; 3], [1e200, 0.0, 0.0], [0.0, 1e200, 0.0]],
|
||||
};
|
||||
assert!(matches!(
|
||||
plane_of(&[huge], 1e-6),
|
||||
Err(SolidError::Failed(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_input_is_refused() {
|
||||
assert!(plane_of(&[], 1e-6).is_err());
|
||||
let line = Curve::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0, 0.0, 0.0],
|
||||
};
|
||||
assert!(plane_of(std::slice::from_ref(&line), 0.0).is_err());
|
||||
assert!(plane_of(&[line], f64::NAN).is_err());
|
||||
let bad = Curve::RationalSpline {
|
||||
knots: vec![0.0, 0.0, 1.0, 1.0],
|
||||
homogeneous: vec![[0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, -1.0]],
|
||||
};
|
||||
assert!(plane_of(&[bad], 1e-6).is_err());
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -80,6 +80,8 @@
|
||||
//! `sm_status` and the status numbers here move to a comparison-only module (or to
|
||||
//! difftest and the FFI), since nothing but the comparison needs them.
|
||||
|
||||
#[cfg(test)]
|
||||
mod bare_scan;
|
||||
pub mod blend;
|
||||
pub mod boolean;
|
||||
pub mod curve;
|
||||
@@ -2220,11 +2222,8 @@ mod tests {
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
// this test's own needles sit in the test tail
|
||||
let library = match text.find("#[cfg(test)]\nmod tests") {
|
||||
Some(at) => &text[..at],
|
||||
None => &text[..],
|
||||
};
|
||||
// this test's own needles sit in its test module
|
||||
let library = bare_scan::library_text(&text);
|
||||
for needle in [
|
||||
"SolidError::Nlib",
|
||||
"Self::Nlib",
|
||||
@@ -2248,10 +2247,7 @@ mod tests {
|
||||
let text = std::fs::read_to_string(src.join(rel))
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
match text.find("#[cfg(test)]\nmod tests") {
|
||||
Some(at) => text[..at].to_owned(),
|
||||
None => text,
|
||||
}
|
||||
bare_scan::library_code(&text)
|
||||
};
|
||||
for rel in [
|
||||
"nurbs/b_spline_curve_n_lib.rs",
|
||||
|
||||
+477
@@ -0,0 +1,477 @@
|
||||
//! Test-only scanner behind every typed-errors ratchet: it finds the bare statuses
|
||||
//! (`SolidError::sm_err(`, `SolidError::null_pointer(`, `SolidError::Upstream {`,
|
||||
//! `SolidError::nl_ser(`) that an area's library code still builds.
|
||||
//!
|
||||
//! A file's library code is what is left once comments, string, raw-string and char
|
||||
//! literals are blanked and every `#[cfg(test)]` (or `#[cfg(all(test, ..))]`) item is
|
||||
//! removed, wherever it sits: a test module in the middle of a file, a test-only `fn` or
|
||||
//! `impl`, or an out-of-line `mod x;` whose file is then skipped. `#![cfg(test)]` makes
|
||||
//! the whole file test code. Blanking keeps every byte offset and line break, so the
|
||||
//! braces of what is left still balance.
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The constructions of a bare status.
|
||||
const NEEDLES: [&str; 4] = [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
"SolidError::nl_ser(",
|
||||
];
|
||||
|
||||
fn is_ident(b: u8) -> bool {
|
||||
b.is_ascii_alphanumeric() || b == b'_'
|
||||
}
|
||||
|
||||
/// The ranges of `text` that hold comments or the contents of string, raw-string,
|
||||
/// byte-string and char literals (not lifetimes).
|
||||
fn literal_ranges(text: &str) -> Vec<Range<usize>> {
|
||||
let t = text.as_bytes();
|
||||
let n = t.len();
|
||||
let at = |i: usize| t.get(i).copied().unwrap_or(0);
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while i < n {
|
||||
let c = at(i);
|
||||
let after_ident = i > 0 && is_ident(at(i - 1));
|
||||
if c == b'/' && at(i + 1) == b'/' {
|
||||
let end = text[i..].find('\n').map_or(n, |k| i + k);
|
||||
out.push(i..end);
|
||||
i = end;
|
||||
} else if c == b'/' && at(i + 1) == b'*' {
|
||||
let (mut depth, mut j) = (1, i + 2);
|
||||
while j < n && depth > 0 {
|
||||
if at(j) == b'/' && at(j + 1) == b'*' {
|
||||
depth += 1;
|
||||
j += 2;
|
||||
} else if at(j) == b'*' && at(j + 1) == b'/' {
|
||||
depth -= 1;
|
||||
j += 2;
|
||||
} else {
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
out.push(i..j);
|
||||
i = j;
|
||||
} else if (c == b'r' || c == b'b') && !after_ident && raw_string_open(t, i).is_some() {
|
||||
let (open, hashes) = raw_string_open(t, i).unwrap_or((i, 0));
|
||||
let close = format!("\"{}", "#".repeat(hashes));
|
||||
let end = text[open..]
|
||||
.find(&close)
|
||||
.map_or(n, |k| open + k + close.len());
|
||||
out.push(open..end.saturating_sub(close.len()).max(open));
|
||||
i = end;
|
||||
} else if c == b'"' || (c == b'b' && at(i + 1) == b'"' && !after_ident) {
|
||||
let mut j = i + if c == b'b' { 2 } else { 1 };
|
||||
let start = j;
|
||||
while j < n && at(j) != b'"' {
|
||||
j += if at(j) == b'\\' { 2 } else { 1 };
|
||||
}
|
||||
out.push(start..j.min(n));
|
||||
i = j + 1;
|
||||
} else if c == b'\'' {
|
||||
match char_literal_end(text, i) {
|
||||
Some(end) => {
|
||||
out.push(i + 1..end - 1);
|
||||
i = end;
|
||||
}
|
||||
None => i += 1,
|
||||
}
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// For a raw string `r#"`/`br#"` starting at `i`: the offset just past its opening quote
|
||||
/// and the number of `#`s.
|
||||
fn raw_string_open(t: &[u8], i: usize) -> Option<(usize, usize)> {
|
||||
let mut j = i;
|
||||
if t.get(j) == Some(&b'b') {
|
||||
j += 1;
|
||||
}
|
||||
if t.get(j) != Some(&b'r') {
|
||||
return None;
|
||||
}
|
||||
j += 1;
|
||||
let hashes = t[j..].iter().take_while(|&&b| b == b'#').count();
|
||||
j += hashes;
|
||||
(t.get(j) == Some(&b'"')).then_some((j + 1, hashes))
|
||||
}
|
||||
|
||||
/// The offset just past a char literal starting with the `'` at `i`, or `None` for a
|
||||
/// lifetime or label.
|
||||
fn char_literal_end(text: &str, i: usize) -> Option<usize> {
|
||||
let rest = text.get(i + 1..)?;
|
||||
let mut chars = rest.char_indices();
|
||||
let (_, first) = chars.next()?;
|
||||
let body = match first {
|
||||
'\\' => {
|
||||
let (_, kind) = chars.next()?;
|
||||
match kind {
|
||||
'x' => 4,
|
||||
'u' => 2 + rest[2..].find('}')? + 1,
|
||||
_ => 1 + kind.len_utf8(),
|
||||
}
|
||||
}
|
||||
'\'' | '\n' => return None,
|
||||
other => other.len_utf8(),
|
||||
};
|
||||
(rest.as_bytes().get(body) == Some(&b'\'')).then_some(i + 1 + body + 1)
|
||||
}
|
||||
|
||||
/// `text` with the bytes in `ranges` replaced by spaces (line breaks kept).
|
||||
fn blank(text: &str, ranges: &[Range<usize>]) -> String {
|
||||
let mut bytes = text.as_bytes().to_vec();
|
||||
for range in ranges {
|
||||
for b in bytes.get_mut(range.clone()).into_iter().flatten() {
|
||||
if *b != b'\n' {
|
||||
*b = b' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
/// The offset just past the closer matching the opener at `i` of `s`.
|
||||
fn match_close(s: &[u8], mut i: usize) -> usize {
|
||||
let mut depth = 0usize;
|
||||
while let Some(&ch) = s.get(i) {
|
||||
match ch {
|
||||
b'(' | b'[' | b'{' => depth += 1,
|
||||
b')' | b']' | b'}' => {
|
||||
depth = depth.saturating_sub(1);
|
||||
if depth == 0 {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
s.len()
|
||||
}
|
||||
|
||||
/// The offset just past any attributes starting at or after `i`.
|
||||
fn skip_attributes(s: &[u8], mut i: usize) -> usize {
|
||||
loop {
|
||||
while s.get(i).is_some_and(u8::is_ascii_whitespace) {
|
||||
i += 1;
|
||||
}
|
||||
if s.get(i) == Some(&b'#') && s.get(i + 1) == Some(&b'[') {
|
||||
i = match_close(s, i + 1);
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The end of the item that starts at `i`, just after its `#[cfg(test)]`: past its
|
||||
/// matching `}`, or past a `;`/`,` at depth 0, or where its container closes.
|
||||
fn item_end(s: &[u8], i: usize) -> usize {
|
||||
let mut i = skip_attributes(s, i);
|
||||
let mut depth = 0usize;
|
||||
while let Some(&ch) = s.get(i) {
|
||||
match ch {
|
||||
b'{' if depth == 0 => return match_close(s, i),
|
||||
b'(' | b'[' | b'{' => depth += 1,
|
||||
b')' | b']' | b'}' if depth == 0 => return i,
|
||||
b')' | b']' | b'}' => depth -= 1,
|
||||
b';' | b',' if depth == 0 => return i + 1,
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
s.len()
|
||||
}
|
||||
|
||||
/// Whether the attribute `#[..]`/`#![..]` (blanked code, `#` at `i`) is a test cfg, and
|
||||
/// where it ends; `inner` is set for `#!`.
|
||||
fn test_cfg_at(s: &[u8], i: usize) -> Option<(usize, bool)> {
|
||||
let inner = s.get(i + 1) == Some(&b'!');
|
||||
let open = i + 1 + usize::from(inner);
|
||||
if s.get(open) != Some(&b'[') {
|
||||
return None;
|
||||
}
|
||||
let end = match_close(s, open);
|
||||
let condensed: String = s
|
||||
.get(open + 1..end.saturating_sub(1))?
|
||||
.iter()
|
||||
.filter(|b| !b.is_ascii_whitespace())
|
||||
.map(|&b| char::from(b))
|
||||
.collect();
|
||||
let test = condensed == "cfg(test)"
|
||||
|| condensed == "cfg(all(test))"
|
||||
|| condensed.starts_with("cfg(all(test,");
|
||||
test.then_some((end, inner))
|
||||
}
|
||||
|
||||
/// The name of an out-of-line module declared by `item` (`mod x;`, with any visibility).
|
||||
fn out_of_line_mod(item: &str) -> Option<String> {
|
||||
let mut rest = item.trim_start();
|
||||
if let Some(after) = rest.strip_prefix("pub") {
|
||||
rest = after.trim_start();
|
||||
if rest.starts_with('(') {
|
||||
rest = &rest[rest.find(')')? + 1..];
|
||||
}
|
||||
rest = rest.trim_start();
|
||||
}
|
||||
let rest = rest.strip_prefix("mod")?;
|
||||
if !rest.starts_with(char::is_whitespace) {
|
||||
return None;
|
||||
}
|
||||
let rest = rest.trim_start();
|
||||
let name_len = rest.bytes().take_while(|&b| is_ident(b)).count();
|
||||
(name_len > 0 && rest[name_len..].trim() == ";").then(|| rest[..name_len].to_owned())
|
||||
}
|
||||
|
||||
/// A file split into its library part and its test part.
|
||||
struct Split {
|
||||
/// The ranges taken by test-only items.
|
||||
tests: Vec<Range<usize>>,
|
||||
/// The out-of-line modules the file declares test-only.
|
||||
test_mods: Vec<String>,
|
||||
}
|
||||
|
||||
fn split(code: &str) -> Split {
|
||||
let s = code.as_bytes();
|
||||
let mut tests = Vec::new();
|
||||
let mut test_mods = Vec::new();
|
||||
let mut pos = 0;
|
||||
while let Some(k) = code[pos..].find('#') {
|
||||
let at = pos + k;
|
||||
match test_cfg_at(s, at) {
|
||||
Some((_, true)) => {
|
||||
return Split {
|
||||
tests: std::iter::once(0..s.len()).collect(),
|
||||
test_mods,
|
||||
};
|
||||
}
|
||||
Some((attr_end, false)) => {
|
||||
let end = item_end(s, attr_end);
|
||||
let item_start = skip_attributes(s, attr_end);
|
||||
if let Some(name) = code.get(item_start..end).and_then(out_of_line_mod) {
|
||||
test_mods.push(name);
|
||||
}
|
||||
tests.push(at..end);
|
||||
pos = end.max(at + 1);
|
||||
}
|
||||
None => pos = at + 1,
|
||||
}
|
||||
}
|
||||
Split { tests, test_mods }
|
||||
}
|
||||
|
||||
/// The library code of a source file: comments and literal contents blanked, and every
|
||||
/// test-only item removed. This is what the needles are counted in.
|
||||
pub(in crate::error) fn library_code(text: &str) -> String {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
let code = blank(&text, &literal_ranges(&text));
|
||||
let tests = split(&code).tests;
|
||||
blank(&code, &tests)
|
||||
}
|
||||
|
||||
/// The library part of a source file with its comments and strings kept: every
|
||||
/// test-only item removed, for checks on message text.
|
||||
pub(in crate::error) fn library_text(text: &str) -> String {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
let code = blank(&text, &literal_ranges(&text));
|
||||
blank(&text, &split(&code).tests)
|
||||
}
|
||||
|
||||
/// The bare statuses library code (from [`library_code`]) builds.
|
||||
pub(in crate::error) fn bare_sites(code: &str) -> usize {
|
||||
NEEDLES
|
||||
.iter()
|
||||
.map(|needle| code.matches(needle).count())
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The library files of an area, `src/<area>.rs` and everything under `src/<area>/`,
|
||||
/// as (path relative to `src` with `/` separators, text with LF line ends), sorted by
|
||||
/// path. Test files (`tests.rs`, `*_tests.rs`, anything under a `tests/` directory) and
|
||||
/// the files of modules declared `#[cfg(test)] mod x;` are skipped. At least `least`
|
||||
/// `.rs` files must be found.
|
||||
pub(in crate::error) fn library_files(area: &str, least: usize) -> Vec<(String, String)> {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut paths: Vec<PathBuf> = vec![root.join(format!("{area}.rs"))];
|
||||
let mut dirs = vec![root.join(area)];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("read dir") {
|
||||
let path = entry.expect("entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(paths.len() >= least, "{paths:?}");
|
||||
let mut files: Vec<(String, String)> = paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let relative = path
|
||||
.strip_prefix(&root)
|
||||
.expect("under src")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
let text = std::fs::read_to_string(path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
(relative, text)
|
||||
})
|
||||
.collect();
|
||||
files.sort();
|
||||
// the directories holding modules declared test-only
|
||||
let mut test_only: Vec<String> = Vec::new();
|
||||
for (relative, text) in &files {
|
||||
let stem = relative.trim_end_matches(".rs");
|
||||
let dir = match stem.strip_suffix("/mod") {
|
||||
Some(parent) => parent.to_owned(),
|
||||
None => stem.to_owned(),
|
||||
};
|
||||
let code = blank(text, &literal_ranges(text));
|
||||
for name in split(&code).test_mods {
|
||||
test_only.push(format!("{dir}/{name}"));
|
||||
}
|
||||
}
|
||||
files.retain(|(relative, _)| {
|
||||
let stem = relative.trim_end_matches(".rs");
|
||||
!(relative.ends_with("/tests.rs")
|
||||
|| relative.ends_with("_tests.rs")
|
||||
|| relative.contains("/tests/")
|
||||
|| test_only
|
||||
.iter()
|
||||
.any(|m| stem == m || stem.starts_with(&format!("{m}/"))))
|
||||
});
|
||||
files
|
||||
}
|
||||
|
||||
/// The library files of an area that build bare statuses, with the number each builds,
|
||||
/// sorted by path (see [`library_files`] for the files read).
|
||||
pub(in crate::error) fn bare_statuses_in(area: &str, least: usize) -> Vec<(String, usize)> {
|
||||
library_files(area, least)
|
||||
.into_iter()
|
||||
.filter_map(|(relative, text)| {
|
||||
let bare = bare_sites(&library_code(&text));
|
||||
(bare > 0).then_some((relative, bare))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Asserts that an area's library files build exactly the bare statuses listed
|
||||
/// (path, count; sorted by path, every count above 0) and none elsewhere.
|
||||
pub(in crate::error) fn assert_waiting(area: &str, least: usize, waiting: &[(&str, usize)]) {
|
||||
let expected: Vec<(String, usize)> = waiting
|
||||
.iter()
|
||||
.map(|(file, n)| ((*file).to_owned(), *n))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
bare_statuses_in(area, least),
|
||||
expected,
|
||||
"bare statuses per {area} file"
|
||||
);
|
||||
for (file, n) in waiting {
|
||||
assert!(*n > 0, "{file} holds no bare status: remove its line");
|
||||
}
|
||||
assert!(
|
||||
waiting.windows(2).all(|w| w[0].0 < w[1].0),
|
||||
"the {area} waiting list is sorted by path, one file per line"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const B: &str = "SolidError::sm_err(OP)";
|
||||
|
||||
/// Library code is counted however the test items interleave with it, and nothing
|
||||
/// in test items, comments or literals is.
|
||||
#[test]
|
||||
fn library_code_sees_past_every_test_item() {
|
||||
let fixture = format!(
|
||||
r##"
|
||||
fn a() -> E {{ {B} }}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {{
|
||||
fn t() {{ {B}; let _ = '}}'; }}
|
||||
}}
|
||||
|
||||
fn b() -> E {{ SolidError::null_pointer(OP) }}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) mod more_tests {{
|
||||
fn t() {{ {B} }}
|
||||
}}
|
||||
|
||||
#[cfg(all(test, feature = "x"))]
|
||||
fn helper() {{ {B} }}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Thing {{
|
||||
fn t() {{ {B} }}
|
||||
}}
|
||||
|
||||
#[cfg(test)]
|
||||
mod exact;
|
||||
|
||||
fn c() -> E {{
|
||||
// a comment: {B} and an unbalanced {{
|
||||
/* nested /* {B} */ still a comment }} */
|
||||
let s = "a string {B} with {{ and \" quote";
|
||||
let r = r#"raw {B} with "}}" inside"#;
|
||||
let o = '{{';
|
||||
let q = '\'';
|
||||
let l: &'static str = "";
|
||||
SolidError::Upstream {{ code: 1 }}
|
||||
}}
|
||||
|
||||
#[cfg(test)]
|
||||
mod last {{ fn t() {{ {B} }} }}
|
||||
|
||||
fn d() -> E {{ SolidError::nl_ser(OP) }}
|
||||
"##
|
||||
);
|
||||
let code = library_code(&fixture);
|
||||
assert_eq!(bare_sites(&code), 4, "{code}");
|
||||
assert_eq!(bare_sites(&library_code(&fixture.replace('\n', "\r\n"))), 4);
|
||||
// braces still balance once literals and comments are blanked
|
||||
let opens = code.matches('{').count();
|
||||
assert_eq!(opens, code.matches('}').count(), "{code}");
|
||||
let code = fixture.replace("\r\n", "\n");
|
||||
let blanked = blank(&code, &literal_ranges(&code));
|
||||
assert_eq!(split(&blanked).test_mods, vec!["exact".to_owned()]);
|
||||
// the text form keeps comments and strings of library code only
|
||||
let text = library_text(&fixture);
|
||||
assert!(text.contains("a string SolidError::sm_err(OP)"));
|
||||
assert!(!text.contains("fn t()"));
|
||||
}
|
||||
|
||||
/// An inner `#![cfg(test)]` makes the whole file test code.
|
||||
#[test]
|
||||
fn an_inner_test_cfg_drops_the_whole_file() {
|
||||
let fixture = format!("#![cfg(test)]\nfn a() -> E {{ {B} }}\n");
|
||||
assert_eq!(bare_sites(&library_code(&fixture)), 0);
|
||||
}
|
||||
|
||||
/// Lifetimes and labels are not char literals; byte and escaped chars are.
|
||||
#[test]
|
||||
fn lifetimes_are_not_char_literals() {
|
||||
let fixture = format!(
|
||||
"fn a<'a>(x: &'a str) -> E {{ 'outer: loop {{ break 'outer; }} \
|
||||
let _ = (b'{{', '\\u{{7b}}', '\\x7b', 'é'); {B} }}\n"
|
||||
);
|
||||
let code = library_code(&fixture);
|
||||
assert_eq!(bare_sites(&code), 1, "{code}");
|
||||
assert_eq!(
|
||||
code.matches('{').count(),
|
||||
code.matches('}').count(),
|
||||
"{code}"
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-13
@@ -292,15 +292,6 @@ mod tests {
|
||||
#[test]
|
||||
fn the_blend_routines_build_no_bare_statuses() {
|
||||
const WAITING: &[(&str, usize)] = &[
|
||||
("blend/blend_corner.rs", 5),
|
||||
("blend/blend_corner/ball.rs", 15),
|
||||
("blend/blend_corner/chamfer.rs", 8),
|
||||
("blend/blend_corner/end.rs", 9),
|
||||
("blend/blend_corner/mixed.rs", 25),
|
||||
("blend/blend_corner/mixed/algebra.rs", 1),
|
||||
("blend/blend_corner/setback.rs", 27),
|
||||
("blend/blend_corner/swept.rs", 7),
|
||||
("blend/blend_setback.rs", 4),
|
||||
("blend/fillet_corner/p1.rs", 46),
|
||||
("blend/fillet_corner/p2.rs", 17),
|
||||
("blend/fillet_corner/p3.rs", 25),
|
||||
@@ -318,8 +309,6 @@ mod tests {
|
||||
("blend/fillet_corner/topology/swept_turn.rs", 3),
|
||||
("blend/fillet_custom_solver.rs", 20),
|
||||
("blend/fillet_executive/session/finish.rs", 2),
|
||||
("blend/fillet_geom/p1.rs", 62),
|
||||
("blend/fillet_geom/p2.rs", 106),
|
||||
("blend/fillet_geom/p3.rs", 27),
|
||||
("blend/fillet_geom/p4.rs", 63),
|
||||
("blend/fillet_geom/p5.rs", 113),
|
||||
@@ -332,7 +321,7 @@ mod tests {
|
||||
("blend/fillet_solver/p1/rails.rs", 7),
|
||||
("blend/fillet_solver/p2.rs", 8),
|
||||
("blend/fillet_solver/p3.rs", 18),
|
||||
("blend/fillet_solver/p4.rs", 6),
|
||||
("blend/fillet_solver/p4.rs", 38),
|
||||
("blend/fillet_solver/p5.rs", 7),
|
||||
("blend/fillet_solver/p5/end_sections_fit.rs", 13),
|
||||
("blend/fillet_solver/p5/rails_fit.rs", 25),
|
||||
@@ -343,7 +332,6 @@ mod tests {
|
||||
("blend/fillet_standard_solver/surface_surface.rs", 2),
|
||||
("blend/fillet_standard_solver/touch_circle.rs", 1),
|
||||
];
|
||||
// the cut at each file's test module is the shared one, `bare_statuses_in`
|
||||
let found = super::super::query::tests::bare_statuses_in("blend", 100);
|
||||
let waiting: Vec<(String, usize)> = WAITING
|
||||
.iter()
|
||||
|
||||
+9
-47
@@ -290,52 +290,14 @@ mod tests {
|
||||
/// whose conversion waits on other open work.
|
||||
#[test]
|
||||
fn the_curve_routines_build_no_bare_statuses() {
|
||||
const WAITING: [(&str, usize); 3] = [
|
||||
("curve/curve/p3.rs", 10),
|
||||
("curve/curve/p7.rs", 3),
|
||||
("curve/ellipse/p1.rs", 6),
|
||||
];
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = vec![root.join("curve.rs")];
|
||||
let mut dirs = vec![root.join("curve")];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("read dir") {
|
||||
let path = entry.expect("entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
assert!(files.len() > 30, "{files:?}");
|
||||
for path in files {
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.find("\n#[cfg(test)]\nmod ")
|
||||
.map_or(text.as_str(), |at| &text[..at]);
|
||||
let bare: usize = [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
"SolidError::nl_ser(",
|
||||
]
|
||||
.iter()
|
||||
.map(|needle| library.matches(needle).count())
|
||||
.sum();
|
||||
let relative = path
|
||||
.strip_prefix(&root)
|
||||
.expect("under src")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
let allowed = WAITING
|
||||
.iter()
|
||||
.find(|(file, _)| *file == relative)
|
||||
.map_or(0, |(_, n)| *n);
|
||||
assert_eq!(bare, allowed, "bare statuses in {relative}");
|
||||
}
|
||||
super::super::bare_scan::assert_waiting(
|
||||
"curve",
|
||||
31,
|
||||
&[
|
||||
("curve/curve/p3.rs", 10),
|
||||
("curve/curve/p7.rs", 3),
|
||||
("curve/ellipse/p1.rs", 6),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-24
@@ -158,30 +158,10 @@ mod tests {
|
||||
/// their messages name no SMLib API.
|
||||
#[test]
|
||||
fn the_heal_routines_build_no_bare_statuses() {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/heal");
|
||||
for rel in [
|
||||
"gap.rs",
|
||||
"face_props.rs",
|
||||
"heal_data/p1.rs",
|
||||
"heal_data/p2.rs",
|
||||
"heal_data/p3.rs",
|
||||
"stitch/p1.rs",
|
||||
"stitch/p2.rs",
|
||||
] {
|
||||
let text = std::fs::read_to_string(src.join(rel))
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.find("#[cfg(test)]\nmod tests")
|
||||
.map_or(text.as_str(), |at| &text[..at]);
|
||||
for needle in [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
"\"SmApi",
|
||||
] {
|
||||
assert!(!library.contains(needle), "{needle} in {rel}");
|
||||
}
|
||||
super::super::bare_scan::assert_waiting("heal", 7, &[]);
|
||||
for (rel, text) in super::super::bare_scan::library_files("heal", 7) {
|
||||
let library = super::super::bare_scan::library_text(&text);
|
||||
assert!(!library.contains("\"SmApi"), "\"SmApi in {rel}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-77
@@ -212,82 +212,13 @@ pub(super) mod tests {
|
||||
/// hold) whose conversion waits on other open work.
|
||||
#[test]
|
||||
fn the_intersect_routines_build_no_bare_statuses() {
|
||||
const WAITING: [(&str, usize); 2] = [
|
||||
("intersect/surface_intersector/p1.rs", 6),
|
||||
("intersect/surface_intersector/p5.rs", 6),
|
||||
];
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
assert_eq!(bare_statuses_under(&root, "intersect", 40), WAITING);
|
||||
}
|
||||
|
||||
/// The files under `src/<area>` (and `src/<area>.rs`) whose library code builds bare
|
||||
/// statuses, with the number each builds, in path order; at least `least` files are read.
|
||||
pub(in crate::error) fn bare_statuses_under(
|
||||
root: &std::path::Path,
|
||||
area: &str,
|
||||
least: usize,
|
||||
) -> Vec<(&'static str, usize)> {
|
||||
let mut files = vec![root.join(format!("{area}.rs"))];
|
||||
let mut dirs = vec![root.join(area)];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("read dir") {
|
||||
let path = entry.expect("entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
assert!(files.len() >= least, "{files:?}");
|
||||
let mut found = Vec::new();
|
||||
for path in files {
|
||||
let relative = path
|
||||
.strip_prefix(root)
|
||||
.expect("under src")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if relative.ends_with("/tests.rs") || relative.ends_with("_tests.rs") {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
// The library part ends at the test module: `#[cfg(test)]`, then any attributes
|
||||
// (such as an `#[allow(..)]` for the tests' unwraps), then `mod` with any
|
||||
// visibility.
|
||||
let library = text
|
||||
.match_indices("\n#[cfg(test)]\n")
|
||||
.find(|(at, _)| {
|
||||
let mut rest = text[at + "\n#[cfg(test)]\n".len()..].lines();
|
||||
rest.find(|line| {
|
||||
let line = line.trim_start();
|
||||
!(line.starts_with("#[") || line.starts_with("clippy::") || line == ")]")
|
||||
})
|
||||
.is_some_and(|line| {
|
||||
let line = line
|
||||
.strip_prefix("pub(crate) ")
|
||||
.or_else(|| line.strip_prefix("pub(super) "))
|
||||
.or_else(|| line.strip_prefix("pub "))
|
||||
.unwrap_or(line);
|
||||
line.starts_with("mod ")
|
||||
})
|
||||
})
|
||||
.map_or(text.as_str(), |(at, _)| &text[..at]);
|
||||
let bare: usize = [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
"SolidError::nl_ser(",
|
||||
]
|
||||
.iter()
|
||||
.map(|needle| library.matches(needle).count())
|
||||
.sum();
|
||||
if bare > 0 {
|
||||
found.push((&*relative.leak(), bare));
|
||||
}
|
||||
}
|
||||
found
|
||||
super::super::bare_scan::assert_waiting(
|
||||
"intersect",
|
||||
40,
|
||||
&[
|
||||
("intersect/surface_intersector/p1.rs", 6),
|
||||
("intersect/surface_intersector/p5.rs", 6),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-16
@@ -160,21 +160,6 @@ mod tests {
|
||||
/// code.
|
||||
#[test]
|
||||
fn the_mass_routines_build_no_bare_statuses() {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/mass");
|
||||
for rel in ["integrator.rs", "integrator/properties.rs", "enclosure.rs"] {
|
||||
let text = std::fs::read_to_string(src.join(rel))
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.find("#[cfg(test)]\nmod tests")
|
||||
.map_or(text.as_str(), |at| &text[..at]);
|
||||
for needle in [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
] {
|
||||
assert!(!library.contains(needle), "{needle} in {rel}");
|
||||
}
|
||||
}
|
||||
super::super::bare_scan::assert_waiting("mass", 3, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-12
@@ -664,18 +664,17 @@ mod tests {
|
||||
/// hold) whose conversion waits on other open work.
|
||||
#[test]
|
||||
fn the_offset_routines_build_no_bare_statuses() {
|
||||
const WAITING: [(&str, usize); 6] = [
|
||||
("offset/offset_executive/p1.rs", 9),
|
||||
("offset/offset_executive/p2.rs", 9),
|
||||
("offset/offset_executive/reach.rs", 1),
|
||||
("offset/offset_executive/step_gaps.rs", 1),
|
||||
("offset/offset_faces.rs", 4),
|
||||
("offset/offset_geometry_creation.rs", 17),
|
||||
];
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
assert_eq!(
|
||||
super::super::intersect::tests::bare_statuses_under(&root, "offset", 20),
|
||||
WAITING
|
||||
super::super::bare_scan::assert_waiting(
|
||||
"offset",
|
||||
20,
|
||||
&[
|
||||
("offset/offset_executive/p1.rs", 9),
|
||||
("offset/offset_executive/p2.rs", 9),
|
||||
("offset/offset_executive/reach.rs", 1),
|
||||
("offset/offset_executive/step_gaps.rs", 1),
|
||||
("offset/offset_faces.rs", 4),
|
||||
("offset/offset_geometry_creation.rs", 17),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-63
@@ -171,69 +171,8 @@ pub(super) mod tests {
|
||||
|
||||
/// The files under `src/<area>` (and `src/<area>.rs`) whose library code builds bare
|
||||
/// statuses, with the number each builds, sorted by path; at least `least` files are
|
||||
/// read. Test files (`tests.rs`, `*_tests.rs`) are skipped, and the library part of a
|
||||
/// file ends at its test module: `#[cfg(test)]`, then any attributes (such as an
|
||||
/// `#[allow(..)]` for the tests' unwraps), then `mod` with any visibility.
|
||||
/// read. Kept for the ratchets that call it; the scan is [`super::super::bare_scan`].
|
||||
pub(in crate::error) fn bare_statuses_in(area: &str, least: usize) -> Vec<(String, usize)> {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = vec![root.join(format!("{area}.rs"))];
|
||||
let mut dirs = vec![root.join(area)];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("read dir") {
|
||||
let path = entry.expect("entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(files.len() >= least, "{files:?}");
|
||||
let mut found = Vec::new();
|
||||
for path in files {
|
||||
let relative = path
|
||||
.strip_prefix(&root)
|
||||
.expect("under src")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if relative.ends_with("/tests.rs") || relative.ends_with("_tests.rs") {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.match_indices("\n#[cfg(test)]\n")
|
||||
.find(|(at, _)| {
|
||||
let mut rest = text[at + "\n#[cfg(test)]\n".len()..].lines();
|
||||
rest.find(|line| {
|
||||
let line = line.trim_start();
|
||||
!(line.starts_with("#[") || line.starts_with("clippy::") || line == ")]")
|
||||
})
|
||||
.is_some_and(|line| {
|
||||
let line = line
|
||||
.strip_prefix("pub(crate) ")
|
||||
.or_else(|| line.strip_prefix("pub(super) "))
|
||||
.or_else(|| line.strip_prefix("pub "))
|
||||
.unwrap_or(line);
|
||||
line.starts_with("mod ")
|
||||
})
|
||||
})
|
||||
.map_or(text.as_str(), |(at, _)| &text[..at]);
|
||||
let bare: usize = [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
"SolidError::nl_ser(",
|
||||
]
|
||||
.iter()
|
||||
.map(|needle| library.matches(needle).count())
|
||||
.sum();
|
||||
if bare > 0 {
|
||||
found.push((relative, bare));
|
||||
}
|
||||
}
|
||||
found.sort();
|
||||
found
|
||||
super::super::bare_scan::bare_statuses_in(area, least)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-56
@@ -267,61 +267,6 @@ mod tests {
|
||||
/// whose conversion waits on other open work or a later part of the slice.
|
||||
#[test]
|
||||
fn the_surface_routines_build_no_bare_statuses() {
|
||||
const WAITING: [(&str, usize); 1] = [("surface/plane/p2.rs", 1)];
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = vec![root.join("surface.rs")];
|
||||
let mut dirs = vec![root.join("surface")];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("read dir") {
|
||||
let path = entry.expect("entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
assert!(files.len() > 50, "{files:?}");
|
||||
for path in files {
|
||||
let relative = path
|
||||
.strip_prefix(&root)
|
||||
.expect("under src")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
if relative.ends_with("/tests.rs") || relative.ends_with("_tests.rs") {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
// The library part ends at the test module: `#[cfg(test)]`, then any attributes
|
||||
// (such as an `#[allow(..)]` for the tests' unwraps), then `mod`.
|
||||
let library = text
|
||||
.match_indices("\n#[cfg(test)]\n")
|
||||
.find(|(at, _)| {
|
||||
let mut rest = text[at + "\n#[cfg(test)]\n".len()..].lines();
|
||||
rest.find(|line| {
|
||||
let line = line.trim_start();
|
||||
!(line.starts_with("#[") || line.starts_with("clippy::") || line == ")]")
|
||||
})
|
||||
.is_some_and(|line| line.starts_with("mod "))
|
||||
})
|
||||
.map_or(text.as_str(), |(at, _)| &text[..at]);
|
||||
let bare: usize = [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
"SolidError::nl_ser(",
|
||||
]
|
||||
.iter()
|
||||
.map(|needle| library.matches(needle).count())
|
||||
.sum();
|
||||
let allowed = WAITING
|
||||
.iter()
|
||||
.find(|(file, _)| *file == relative)
|
||||
.map_or(0, |(_, n)| *n);
|
||||
assert_eq!(bare, allowed, "bare statuses in {relative}");
|
||||
}
|
||||
super::super::bare_scan::assert_waiting("surface", 51, &[("surface/plane/p2.rs", 1)]);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-32
@@ -198,37 +198,6 @@ mod tests {
|
||||
/// waits on other work).
|
||||
#[test]
|
||||
fn the_sweep_routines_build_no_bare_statuses() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut files = vec![root.join("sweep.rs")];
|
||||
let mut dirs = vec![root.join("sweep")];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("read dir") {
|
||||
let path = entry.expect("entry").path();
|
||||
if path.is_dir() {
|
||||
dirs.push(path);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
assert!(files.len() > 20, "{files:?}");
|
||||
for path in files {
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.find("\n#[cfg(test)]\nmod ")
|
||||
.map_or(text.as_str(), |at| &text[..at]);
|
||||
let bare: usize = [
|
||||
"SolidError::sm_err(",
|
||||
"SolidError::null_pointer(",
|
||||
"SolidError::Upstream {",
|
||||
]
|
||||
.iter()
|
||||
.map(|needle| library.matches(needle).count())
|
||||
.sum();
|
||||
assert_eq!(bare, 0, "bare statuses in {}", path.display());
|
||||
}
|
||||
super::super::bare_scan::assert_waiting("sweep", 21, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-77
@@ -288,84 +288,15 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The converted topology routines describe their failures: their library code
|
||||
/// builds no bare `Upstream` status.
|
||||
/// The topology routines describe their failures: their library code builds no bare
|
||||
/// `Upstream` status, except the file listed (with the bare sites it may still hold).
|
||||
#[test]
|
||||
fn the_topology_routines_build_no_bare_statuses() {
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/topology");
|
||||
for (rel, held) in [
|
||||
"api_files.rs",
|
||||
"api_projection.rs",
|
||||
"brep/cutting.rs",
|
||||
"brep/local_operation.rs",
|
||||
"brep/mirror.rs",
|
||||
"brep/p1.rs",
|
||||
"brep/p10.rs",
|
||||
"brep/p2.rs",
|
||||
"brep/p3.rs",
|
||||
"brep/p4.rs",
|
||||
"brep/p5.rs",
|
||||
"brep/p6.rs",
|
||||
"brep/p7.rs",
|
||||
"brep/p8.rs",
|
||||
"brep/planar_section.rs",
|
||||
"brep/validation/assert_valid.rs",
|
||||
"brep/validation/assert_valid/seams.rs",
|
||||
"brep/validation/pointers.rs",
|
||||
"brep/validation/soundness.rs",
|
||||
"brep/vertex_fans.rs",
|
||||
"brep_cache.rs",
|
||||
"brep_constructor.rs",
|
||||
"brep_data.rs",
|
||||
"brep_data_write.rs",
|
||||
"database_io.rs",
|
||||
"edge.rs",
|
||||
"edgeuse/p1.rs",
|
||||
"edgeuse/p2.rs",
|
||||
"edgeuse/p3.rs",
|
||||
"face/p1.rs",
|
||||
"face/p2.rs",
|
||||
"face/p3.rs",
|
||||
"geometry_write.rs",
|
||||
"loopuse.rs",
|
||||
"part_file.rs",
|
||||
"proto_topology.rs",
|
||||
"proto_topology/p1.rs",
|
||||
"proto_topology/p1/api_transform.rs",
|
||||
"proto_topology/p1/assembly.rs",
|
||||
"proto_topology/p1/copy_brep_map.rs",
|
||||
"proto_topology/p1/owning_topology.rs",
|
||||
"proto_topology/p1/proto.rs",
|
||||
"proto_topology/p2.rs",
|
||||
"proto_topology/p2/imprint_ends.rs",
|
||||
"proto_topology/p3.rs",
|
||||
"shell.rs",
|
||||
"topology.rs",
|
||||
"topology_traverser.rs",
|
||||
"trimming_tools.rs",
|
||||
"trimming_tools/curve_loops.rs",
|
||||
"vertex/corner.rs",
|
||||
"vertexuse.rs",
|
||||
]
|
||||
.map(|rel| (rel, 0))
|
||||
.into_iter()
|
||||
// held back: the face-orientation repair's final status 1 (upstream `TRUE`)
|
||||
.chain([("brep/p9.rs", 1)])
|
||||
{
|
||||
let text = std::fs::read_to_string(src.join(rel))
|
||||
.expect("read")
|
||||
.replace("\r\n", "\n");
|
||||
let library = text
|
||||
.find("\n#[cfg(test)]\nmod ")
|
||||
.map_or(text.as_str(), |at| &text[..at]);
|
||||
for needle in ["SolidError::sm_err(", "SolidError::null_pointer("] {
|
||||
assert!(!library.contains(needle), "{needle} in {rel}");
|
||||
}
|
||||
assert_eq!(
|
||||
library.matches("SolidError::Upstream {").count(),
|
||||
held,
|
||||
"bare Upstream statuses in {rel}"
|
||||
);
|
||||
}
|
||||
super::super::bare_scan::assert_waiting(
|
||||
"topology",
|
||||
53,
|
||||
// held back: the face-orientation repair's final status 1 (upstream `TRUE`)
|
||||
&[("topology/brep/p9.rs", 1)],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
-4
@@ -840,10 +840,6 @@ pub struct UpstreamFixes {
|
||||
/// `plan/upstream-issues/WR-24--addendum-2026-10-31-knot-vector-requires-span.md`).
|
||||
/// Off by default for pinned C++ parity.
|
||||
pub knot_vector_valid_requires_span: bool,
|
||||
/// `SmCone::IntersectWithCone` tests the line of two parallel cylinders against both face domains at its middle, not its bottom end (WR-259, #711).
|
||||
pub cone_parallel_line_midpoint: bool,
|
||||
/// `SmCone::IntersectWithCone` clips parallel-cylinder lines to both requested face domains, including overlaps away from the full line's middle (WR-259, #711). Independent of `cone_parallel_line_midpoint`; off by default for pinned SMLib compatibility.
|
||||
pub cone_parallel_line_domains: bool,
|
||||
/// `SmCone::IntersectWithPlane` takes a plane parallel to a cylinder's axis as tangent also when it lies within the zone tolerance of a bounded face's wall along a line (`|d| > R - zone_tol`, the larger of the cone's and the plane's face tolerances, 0 for a surface with no face), besides upstream's two-crossing-lines test, so a plane turned a hair off a face's wall it touches does not cut it in two lines microns apart; bare surfaces are unchanged (WR-510, #1525; tangency-labs/smlib#55, merged as b22bfbc (was bd20f6e)).
|
||||
pub cylinder_plane_tangent_within_tolerance: bool,
|
||||
/// The Boolean's topology intersector treats a self-loop proto edge whose ends a vertex definition on a vertex combined (`CanCombineVertexDefinitions`) as degenerate, instead of `SmProtoEdge::Resolve` splitting it onto that one vertex, after which `SmMerge::MakeFaceuse` returns `SM_ERR` (WR-214, #480; `plan/upstream-issues/WR-214--smprotoedge-resolve-splits-a-self-loop-proto-edge-onto-one.md`).
|
||||
|
||||
@@ -170,6 +170,8 @@ pub fn enclose_volume(k: &Kernel, b: &mut Brep, width: f64) -> Result<VolumeEncl
|
||||
///
|
||||
/// Those of [`enclose_volume`].
|
||||
pub fn volume_sign(k: &Kernel, b: &mut Brep) -> Result<VolumeSign> {
|
||||
// each decision a call of this scope (feature `profile`; nothing without it)
|
||||
crate::profile::profile_scope!(k, "mass::enclosure::volume_sign");
|
||||
volume_sign_within(k, b, CELL_BUDGET)
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -16,6 +16,7 @@ pub mod crv_adv;
|
||||
pub mod crv_approx;
|
||||
pub mod crv_basic;
|
||||
pub mod crv_common;
|
||||
pub mod crv_deviation;
|
||||
pub mod crv_fit;
|
||||
pub mod crv_geom;
|
||||
pub mod crv_nurb;
|
||||
@@ -25,6 +26,7 @@ pub(crate) mod fit_arrays;
|
||||
pub mod funcs_adv;
|
||||
pub mod funcs_basic;
|
||||
pub mod geom;
|
||||
pub mod gordon;
|
||||
pub mod iges;
|
||||
pub(crate) mod index;
|
||||
pub mod knots;
|
||||
@@ -35,6 +37,7 @@ pub mod spiral;
|
||||
pub mod srf_adv;
|
||||
pub mod srf_basic;
|
||||
pub mod srf_common;
|
||||
pub mod srf_deviation;
|
||||
pub mod srf_fit;
|
||||
pub mod srf_nurb;
|
||||
pub mod srf_offset;
|
||||
|
||||
+70
-1
@@ -11,7 +11,7 @@
|
||||
//! | [`BSplineCurve::calculate_bounding_box`], [`BoundingBoxRequest`], [`BoundingBoxes`] | `CalculateBoundingBox` |
|
||||
//! | [`BSplineCurve::is_planar`], [`Planar`] | `SmCurve::IsPlanar` |
|
||||
//! | [`BSplineCurve::create_analytic_curve`], [`AnalyticCurve`] | `CreateAnalyticCurve` |
|
||||
//! | [`BSplineCurve::create_by_joining`] | `CreateByJoining` |
|
||||
//! | [`BSplineCurve::create_by_joining`], [`BSplineCurve::create_by_joining_with_breaks`] | `CreateByJoining` |
|
||||
//! | [`BSplineCurve::create_point_curve`] | `CreatePointCurve` |
|
||||
//! | [`BSplineCurve::create_simple_offset`] | `CreateSimpleOffset` |
|
||||
//! | [`BSplineCurve::has_repeated_end_control_points`] | `HasRepeatedEndControlPoints` |
|
||||
@@ -43,6 +43,9 @@
|
||||
//! `p1`). `CalculateBoundingBox`'s unused `bExpandBox` flag is dropped and the boxes
|
||||
//! asked for come back in [`BoundingBoxes`] (fresh boxes, as a caller passing new
|
||||
//! `SmExtent3d` / `SmPseudoBox` / `SmPolarBox` objects gets).
|
||||
//! - **Join breaks** (not upstream). [`BSplineCurve::create_by_joining_with_breaks`]
|
||||
//! also returns the knot at which each segment ends, read off the knot vector as it is
|
||||
//! built (the skin's face sources, tangency-labs/solidcore#3139).
|
||||
//! - **Missing NURB.** Where upstream dereferences the `NULL` NURB of a curve built
|
||||
//! without one (`GetOrCreateGwNurbPointer` cannot make it for a plain B-spline), the
|
||||
//! port returns `SM_ERR` ([`BSplineFailure::NoNurbsForm`]). That includes
|
||||
@@ -438,6 +441,34 @@ impl BSplineCurve {
|
||||
Ok(self.degree()? == 3 && self.is_piecewise_c0()?)
|
||||
}
|
||||
|
||||
/// Certified boxes covering the part of the curve over `interval`, one per knot span
|
||||
/// that meets it ([`crate::nurbs::nurbs_crv::crv_span_boxes`]); `None`, meaning no
|
||||
/// certified cover, without a NURB, when the weights, knots or interval cannot be
|
||||
/// read as a cover, or while evaluation beyond the natural interval is enabled.
|
||||
/// Not upstream.
|
||||
#[must_use]
|
||||
pub fn span_boxes(&self, interval: &Extent1d) -> Option<Vec<Extent3d>> {
|
||||
if self.is_out_of_bounds_enabled() {
|
||||
return None;
|
||||
}
|
||||
crate::nurbs::nurbs_crv::crv_span_boxes(self.nurb.as_ref()?, interval)
|
||||
}
|
||||
|
||||
/// The refinable certified cover ([`crate::nurbs::nurbs_crv::CurveCover`]) over
|
||||
/// `interval`, refined `depth` times; `None` under the conditions of
|
||||
/// [`BSplineCurve::span_boxes`]. Not upstream.
|
||||
#[must_use]
|
||||
pub fn cover(
|
||||
&self,
|
||||
interval: &Extent1d,
|
||||
depth: usize,
|
||||
) -> Option<crate::nurbs::nurbs_crv::CurveCover> {
|
||||
if self.is_out_of_bounds_enabled() {
|
||||
return None;
|
||||
}
|
||||
crate::nurbs::nurbs_crv::CurveCover::new(self.nurb.as_ref()?, interval, depth)
|
||||
}
|
||||
|
||||
/// Bounding boxes of the part of the curve over `interval` (clamped to the natural
|
||||
/// interval), built from the control points of the spans it covers
|
||||
/// (`CalculateBoundingBox`). The pseudo box takes its basis from the control point
|
||||
@@ -868,6 +899,34 @@ impl BSplineCurve {
|
||||
/// different dimension or degree; `SM_ERR` without a NURB, for a zero end weight or
|
||||
/// when degree elevation or the canonical construction fails.
|
||||
pub fn create_by_joining(segments: &mut [Self], senses: Option<&[bool]>) -> Result<Self> {
|
||||
Self::join(segments, senses, None)
|
||||
}
|
||||
|
||||
/// [`BSplineCurve::create_by_joining`], also returning where each segment lies in
|
||||
/// the joined curve: the joined curve's first knot, then each segment's last knot,
|
||||
/// in segment order (`segments.len() + 1` values). Segment `i` spans
|
||||
/// `[breaks[i], breaks[i + 1]]`. Each value is one of the joined curve's knots, the
|
||||
/// very value its knot vector holds. The curve is the one
|
||||
/// [`BSplineCurve::create_by_joining`] makes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`BSplineCurve::create_by_joining`].
|
||||
pub fn create_by_joining_with_breaks(
|
||||
segments: &mut [Self],
|
||||
senses: Option<&[bool]>,
|
||||
) -> Result<(Self, Vec<f64>)> {
|
||||
let mut breaks = Vec::new();
|
||||
let joined = Self::join(segments, senses, Some(&mut breaks))?;
|
||||
Ok((joined, breaks))
|
||||
}
|
||||
|
||||
/// `CreateByJoining`, recording the segments' breaks in `breaks` when asked.
|
||||
fn join(
|
||||
segments: &mut [Self],
|
||||
senses: Option<&[bool]>,
|
||||
mut breaks: Option<&mut Vec<f64>>,
|
||||
) -> Result<Self> {
|
||||
let mut ctrl_pts: Vec<Point3> = Vec::new();
|
||||
let mut weights: Vec<f64> = Vec::new();
|
||||
let mut knot_mult: Vec<usize> = Vec::new();
|
||||
@@ -966,6 +1025,9 @@ impl BSplineCurve {
|
||||
}
|
||||
ctrl_pts.push(last_point);
|
||||
knots.push(first_of(&seg_knots)?);
|
||||
if let Some(b) = breaks.as_mut() {
|
||||
b.push(first_of(&seg_knots)?);
|
||||
}
|
||||
knot_mult.push(
|
||||
*seg_knot_mult
|
||||
.first()
|
||||
@@ -1086,6 +1148,13 @@ impl BSplineCurve {
|
||||
}
|
||||
ctrl_pts.push(pt);
|
||||
}
|
||||
if let Some(b) = breaks.as_mut() {
|
||||
b.push(
|
||||
*knots
|
||||
.last()
|
||||
.ok_or_else(|| invalid(OP_JOIN, "empty joined data"))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp that last knot
|
||||
|
||||
@@ -924,6 +924,30 @@ impl BSplineSurface {
|
||||
self.nurb.as_ref()
|
||||
}
|
||||
|
||||
/// Certified boxes covering the whole surface, one per knot span
|
||||
/// ([`crate::nurbs::nurbs_srf::srf_span_boxes`]); `None`, meaning no certified
|
||||
/// cover, without a NURB, when the weights or knots cannot be read as a cover, or
|
||||
/// while evaluation beyond the natural domain is enabled (the extension leaves the
|
||||
/// convex hull). Not upstream.
|
||||
#[must_use]
|
||||
pub fn span_boxes(&self) -> Option<Vec<Extent3d>> {
|
||||
if self.is_out_of_bounds_enabled() {
|
||||
return None;
|
||||
}
|
||||
crate::nurbs::nurbs_srf::srf_span_boxes(self.gw_nurb()?)
|
||||
}
|
||||
|
||||
/// The refinable certified cover ([`crate::nurbs::nurbs_srf::SurfaceCover`]), refined
|
||||
/// `depth` times; `None` under the conditions of [`BSplineSurface::span_boxes`].
|
||||
/// Not upstream.
|
||||
#[must_use]
|
||||
pub fn cover(&self, depth: usize) -> Option<crate::nurbs::nurbs_srf::SurfaceCover> {
|
||||
if self.is_out_of_bounds_enabled() {
|
||||
return None;
|
||||
}
|
||||
crate::nurbs::nurbs_srf::SurfaceCover::new(self.gw_nurb()?, depth)
|
||||
}
|
||||
|
||||
/// The NURB, made first if missing (`GetOrCreateGwNurbPointer`).
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
//! A certified bound of how far apart two B-spline curves are at the same parameters.
|
||||
//!
|
||||
//! Upstream: none (not upstream; solidcore-native).
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |------------------------|----------|
|
||||
//! | [`certified_distance`] | none |
|
||||
//! | [`interpolation_bound`] | none |
|
||||
//!
|
||||
//! Both curves are cut into Bézier pieces over the union of their breakpoints
|
||||
//! ([`Curve::refine`], [`Curve::decompose_bez`]) and each piece raised to the higher of the
|
||||
//! two degrees ([`Curve::elevate_degree`], on a piece with no interior knot only the Bézier
|
||||
//! elevation). On a common span the difference of two pieces of one degree is the Bézier
|
||||
//! piece of the differences of their control points, which lies in their convex hull
|
||||
//! (Farouki and Rajan 1987; Piegl and Tiller, *The NURBS Book*, 2nd ed., §5.6): at no
|
||||
//! parameter of the span are the curves farther apart than the largest difference.
|
||||
//!
|
||||
//! Rounding (docs/definitions-and-certificates.md §4.4): every new control point of a knot
|
||||
//! insertion is one convex combination of two points, its ratio a quotient of knot
|
||||
//! differences and its complement one subtraction, so each insertion adds at most
|
||||
//! `γ_12 M` per coordinate, `M` the net's largest coordinate (a convex combination neither
|
||||
//! grows the coordinates nor the errors it is given); `I` insertions carry `γ_(12 (I + 1))`
|
||||
//! of twice `M`. Both curves are first moved into their own frame (minus the first control
|
||||
//! point of the first; each moved coordinate rounds once, `γ_2` of `M`), so `M` is their size
|
||||
//! there, not their distance from the origin. The elevation of a piece by `t` forms each point as a convex combination of
|
||||
//! at most `p + 1` points with coefficients that are quotients of binomials: `γ_(8 q)` of
|
||||
//! `M`, `q` the degree reached. The norm of each difference is rounded up by `γ_8`, and a
|
||||
//! coordinate error `e` moves a point by at most `2 e` (`√3 e`).
|
||||
//!
|
||||
//! [`interpolation_bound`] bounds how far a curve misses points it was fitted through at
|
||||
//! their parameters: each point of the curve is evaluated (the basis functions of The NURBS
|
||||
//! Book's A2.3 at order 0, the general basis whatever the knots, and their sum with the
|
||||
//! control points: `γ_(12 p + 4)` of `M` per coordinate,
|
||||
//! the count `eval::collapse` gives de Boor's `p` levels) and its computed distance from the
|
||||
//! data point rounded up by `γ_8`. Every distance is a scaled norm (the components divided by
|
||||
//! the largest before squaring), so a difference near the bottom of the floating range is not
|
||||
//! lost to underflow.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). Non-rational 3D curves only, of degree 1 to
|
||||
//! [`MAX_DEGREE`]; their knot vectors clamped on one domain.
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::crv_basic::Curve;
|
||||
use crate::nurbs::knots::{Side, MAX_DEGREE};
|
||||
use crate::nurbs::math::{gamma, CPoint, NOW, NOZ};
|
||||
|
||||
const OP: &str = "nurbs::crv_deviation::certified_distance";
|
||||
const OP_FIT: &str = "nurbs::crv_deviation::interpolation_bound";
|
||||
|
||||
/// A bound `d` with `|c(u_k) - q_k| <= d` for every point `q_k` of `points` and its
|
||||
/// parameter `u_k` of `params`, every rounding of computing it included (module docs): how
|
||||
/// far the curve fitted through the points misses them.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a rational or 2D curve, a parameter count other than the points', or a
|
||||
/// number not finite;
|
||||
/// a query failure when the bound is not finite; those of the evaluation (a parameter
|
||||
/// outside the knots).
|
||||
pub fn interpolation_bound(c: &Curve, params: &[f64], points: &[Vec3]) -> Result<f64> {
|
||||
if c.cpts().iter().any(|p| p.w != NOW || p.z == NOZ) {
|
||||
return Err(SolidError::invalid_input(OP_FIT, "a rational or 2D curve"));
|
||||
}
|
||||
if params.len() != points.len() {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_FIT,
|
||||
"one parameter per point needed",
|
||||
));
|
||||
}
|
||||
// a NaN distance would be dropped by `f64::max`: every number is checked first
|
||||
if !shaped(c) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_FIT,
|
||||
"a degree of 0 or past the most, or knots that do not fit the points",
|
||||
));
|
||||
}
|
||||
if !finite(c)
|
||||
|| !params.iter().all(|u| u.is_finite())
|
||||
|| !points
|
||||
.iter()
|
||||
.all(|q| q.x.is_finite() && q.y.is_finite() && q.z.is_finite())
|
||||
{
|
||||
return Err(SolidError::invalid_input(OP_FIT, "numbers not finite"));
|
||||
}
|
||||
let size = largest_coordinate(c);
|
||||
let mut largest = 0.0_f64;
|
||||
for (&u, q) in params.iter().zip(points) {
|
||||
// the general basis (derivatives of order 0): the NLib `eval` keeps upstream's
|
||||
// clamped reading of a domain's ends, wrong on a periodic curve's
|
||||
let p = c
|
||||
.derivs(u, Side::Left, 0)?
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::query_failed(OP_FIT, "no point evaluated"))?;
|
||||
let d = norm_up(p.x - q.x, p.y - q.y, p.z - q.z)
|
||||
.ok_or_else(|| SolidError::query_failed(OP_FIT, "the bound is not finite"))?;
|
||||
largest = largest.max(d);
|
||||
}
|
||||
let eval_err = gamma(c.degree().saturating_mul(12).saturating_add(4)) * size;
|
||||
let bound = (largest + 2.0 * eval_err) * (1.0 + gamma(2));
|
||||
if !bound.is_finite() {
|
||||
return Err(SolidError::query_failed(OP_FIT, "the bound is not finite"));
|
||||
}
|
||||
Ok(bound)
|
||||
}
|
||||
|
||||
/// A bound `d` with `|a(t) - b(t)| <= d` at every parameter `t` of the curves' common
|
||||
/// domain, every rounding of computing it included: the largest distance between the
|
||||
/// control points of their Bézier pieces raised to one degree over common spans (module
|
||||
/// docs). A curve and itself, with a knot inserted or its degree raised, are within
|
||||
/// rounding.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a rational or 2D curve, a number not finite, knots not clamped, or two
|
||||
/// curves on different domains; a query failure when their pieces do not share their spans or the bound is not
|
||||
/// finite; those of the refinement, the extraction and the elevation.
|
||||
pub fn certified_distance(a: &Curve, b: &Curve) -> Result<f64> {
|
||||
check(a)?;
|
||||
check(b)?;
|
||||
if a.knots().first() != b.knots().first() || a.knots().last() != b.knots().last() {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the curves are not on one domain",
|
||||
));
|
||||
}
|
||||
// In the curves' own frame: both moved by minus `a`'s first control point, so the
|
||||
// rounding counted below follows their size, not their distance from the origin. The
|
||||
// difference of the two is the same in either frame; each moved coordinate rounds once.
|
||||
let Some(&c0) = a.cpts().first() else {
|
||||
return Err(SolidError::invalid_input(OP, "a curve without points"));
|
||||
};
|
||||
let local = |c: &Curve| -> Result<Curve> {
|
||||
let pw = c
|
||||
.cpts()
|
||||
.iter()
|
||||
.map(|p| CPoint::new(p.x - c0.x, p.y - c0.y, p.z - c0.z, NOW))
|
||||
.collect();
|
||||
Curve::new(pw, c.degree(), c.knots().to_vec())
|
||||
};
|
||||
let (a_local, b_local) = (local(a)?, local(b)?);
|
||||
let (a, b) = (&a_local, &b_local);
|
||||
let q = a.degree().max(b.degree());
|
||||
let size = largest_coordinate(a).max(largest_coordinate(b));
|
||||
let moved = gamma(2) * size;
|
||||
let (pa, ea) = pieces(a, b, q, size)?;
|
||||
let (pb, eb) = pieces(b, a, q, size)?;
|
||||
let (ea, eb) = (ea + moved, eb + moved);
|
||||
if pa.len() != pb.len() {
|
||||
return Err(SolidError::query_failed(
|
||||
OP,
|
||||
"the curves' pieces do not share their spans",
|
||||
));
|
||||
}
|
||||
let mut largest = 0.0_f64;
|
||||
for (x, y) in pa.iter().zip(&pb) {
|
||||
if x.knots().first() != y.knots().first() || x.knots().last() != y.knots().last() {
|
||||
return Err(SolidError::query_failed(
|
||||
OP,
|
||||
"the curves' pieces do not share their spans",
|
||||
));
|
||||
}
|
||||
if x.cpts().len() != y.cpts().len() {
|
||||
return Err(SolidError::query_failed(
|
||||
OP,
|
||||
"the curves' pieces are not of one degree",
|
||||
));
|
||||
}
|
||||
for (p, r) in x.cpts().iter().zip(y.cpts()) {
|
||||
let d = norm_up(p.x - r.x, p.y - r.y, p.z - r.z)
|
||||
.ok_or_else(|| SolidError::query_failed(OP, "the bound is not finite"))?;
|
||||
largest = largest.max(d);
|
||||
}
|
||||
}
|
||||
let bound = (largest + 2.0 * (ea + eb)) * (1.0 + gamma(2));
|
||||
if !bound.is_finite() {
|
||||
return Err(SolidError::query_failed(OP, "the bound is not finite"));
|
||||
}
|
||||
Ok(bound)
|
||||
}
|
||||
|
||||
/// An upper bound of the length of the difference `(dx, dy, dz)` (each one subtraction),
|
||||
/// every rounding included and nothing lost to underflow: the components are divided by the
|
||||
/// largest before they are squared (each quotient rounds by `γ_1`, the squares, sums and
|
||||
/// root by `γ_5`, the product by `γ_1`, the subtractions by `γ_1`: `γ_8` in all), and the
|
||||
/// smallest subnormals a product with a subnormal largest can lose are added back. `None`
|
||||
/// for a difference that overflows.
|
||||
pub(crate) fn norm_up(dx: f64, dy: f64, dz: f64) -> Option<f64> {
|
||||
let s = dx.abs().max(dy.abs()).max(dz.abs());
|
||||
if !s.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if s == 0.0 {
|
||||
return Some(0.0);
|
||||
}
|
||||
let (x, y, z) = (dx / s, dy / s, dz / s);
|
||||
let bound = s * (x * x + y * y + z * z).sqrt() * (1.0 + gamma(8)) + 4.0 * f64::from_bits(1);
|
||||
bound.is_finite().then_some(bound)
|
||||
}
|
||||
|
||||
/// Whether `c`'s degree is one NLib takes (1 to [`MAX_DEGREE`]) and its knots fit its control
|
||||
/// points (their count plus the degree plus one), checked before any arithmetic on the
|
||||
/// degree.
|
||||
fn shaped(c: &Curve) -> bool {
|
||||
let p = c.degree();
|
||||
(1..=MAX_DEGREE).contains(&p) && c.knots().len() == c.cpts().len() + p + 1
|
||||
}
|
||||
|
||||
/// Whether every control point coordinate and knot of `c` is finite.
|
||||
fn finite(c: &Curve) -> bool {
|
||||
c.cpts()
|
||||
.iter()
|
||||
.all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite())
|
||||
&& c.knots().iter().all(|k| k.is_finite())
|
||||
}
|
||||
|
||||
/// Refuses a curve the bound does not take: rational, 2D, with a number not finite, or with
|
||||
/// knots not clamped.
|
||||
fn check(c: &Curve) -> Result<()> {
|
||||
if !finite(c) {
|
||||
return Err(SolidError::invalid_input(OP, "numbers not finite"));
|
||||
}
|
||||
if !shaped(c) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"a degree of 0 or past the most, or knots that do not fit the points",
|
||||
));
|
||||
}
|
||||
if c.cpts().iter().any(|p| p.w != NOW) {
|
||||
return Err(SolidError::invalid_input(OP, "a rational curve"));
|
||||
}
|
||||
if c.cpts().iter().any(|p| p.z == NOZ) {
|
||||
return Err(SolidError::invalid_input(OP, "a 2D curve"));
|
||||
}
|
||||
let p = c.degree();
|
||||
let knots = c.knots();
|
||||
let flat =
|
||||
|run: Option<&[f64]>| run.is_some_and(|r| r.windows(2).all(|w| w.first() == w.get(1)));
|
||||
let last = knots.len().checked_sub(p + 1);
|
||||
if p == 0 || !flat(knots.get(..=p)) || !flat(last.and_then(|s| knots.get(s..))) {
|
||||
return Err(SolidError::invalid_input(OP, "knots not clamped"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The largest absolute coordinate of `c`'s control points.
|
||||
fn largest_coordinate(c: &Curve) -> f64 {
|
||||
c.cpts().iter().fold(0.0_f64, |m, p| {
|
||||
m.max(p.x.abs()).max(p.y.abs()).max(p.z.abs())
|
||||
})
|
||||
}
|
||||
|
||||
/// The Bézier pieces of `c` over the union of its breakpoints and `other`'s, each raised to
|
||||
/// degree `q`, and the error each of their coordinates may carry (module docs), `size` the
|
||||
/// largest coordinate of both nets.
|
||||
fn pieces(c: &Curve, other: &Curve, q: usize, size: f64) -> Result<(Vec<Curve>, f64)> {
|
||||
let (Some(&lo), Some(&hi)) = (c.knots().first(), c.knots().last()) else {
|
||||
return Err(SolidError::invalid_input(OP, "a curve without knots"));
|
||||
};
|
||||
// the breakpoints of `other` that `c` lacks, once each, in order
|
||||
let mut missing: Vec<f64> = Vec::new();
|
||||
for &u in other.knots() {
|
||||
if u > lo && u < hi && !c.knots().contains(&u) && missing.last() != Some(&u) {
|
||||
missing.push(u);
|
||||
}
|
||||
}
|
||||
let refined = if missing.is_empty() {
|
||||
c.clone()
|
||||
} else {
|
||||
c.refine(&missing)?
|
||||
};
|
||||
let p = c.degree();
|
||||
let pieces = refined.decompose_bez()?;
|
||||
// knots inserted: those of the pieces' joined knot vector, 2 (p + 1) + (s - 1) p, past c's
|
||||
let joined = pieces
|
||||
.len()
|
||||
.saturating_sub(1)
|
||||
.saturating_mul(p)
|
||||
.saturating_add(2 * (p + 1));
|
||||
let inserted = joined.saturating_sub(c.knots().len());
|
||||
let mut err = gamma(inserted.saturating_add(1).saturating_mul(12)) * 2.0 * size;
|
||||
let raise = q.saturating_sub(p);
|
||||
if raise == 0 {
|
||||
return Ok((pieces, err));
|
||||
}
|
||||
err += gamma(q.saturating_mul(8)) * (size + err);
|
||||
let raised = pieces
|
||||
.iter()
|
||||
.map(|piece| piece.elevate_degree(raise))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok((raised, err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nurbs::knots::clamped_uniform;
|
||||
|
||||
fn spline(points: &[[f64; 3]], degree: usize) -> Curve {
|
||||
let pw = points
|
||||
.iter()
|
||||
.map(|&[x, y, z]| CPoint::from_point(Vec3::new(x, y, z)))
|
||||
.collect();
|
||||
Curve::new(pw, degree, clamped_uniform(points.len(), degree).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
const WAVE: [[f64; 3]; 6] = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[1.0, 2.0, 0.0],
|
||||
[2.0, -1.0, 0.0],
|
||||
[3.0, 3.0, 0.0],
|
||||
[4.0, 0.0, 1.0],
|
||||
[5.0, 1.0, 0.0],
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn a_curve_is_no_distance_from_itself_or_its_refinements() {
|
||||
let c = spline(&WAVE, 3);
|
||||
let d = certified_distance(&c, &c).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
let more = c.insert_knot(0.3, 2).unwrap();
|
||||
let higher = c.elevate_degree(2).unwrap();
|
||||
for other in [&more, &higher] {
|
||||
let d = certified_distance(&c, other).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
let d = certified_distance(other, &c).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_moved_control_point_is_bounded_by_its_move_and_not_less_than_the_curves_gap() {
|
||||
let c = spline(&WAVE, 3);
|
||||
let mut moved = WAVE;
|
||||
moved[2][2] += 1.0;
|
||||
let m = spline(&moved, 3);
|
||||
let d = certified_distance(&c, &m).unwrap();
|
||||
// the gap is the moved point's basis function times 1: at most 1, and the bound
|
||||
// holds every sampled gap
|
||||
assert!(d <= 1.0 + 1e-12, "{d}");
|
||||
for i in 0..=200 {
|
||||
let t = f64::from(i) / 200.0;
|
||||
let (p, r) = (
|
||||
c.eval(t, Side::Left).unwrap(),
|
||||
m.eval(t, Side::Left).unwrap(),
|
||||
);
|
||||
let gap2 = (p.x - r.x).powi(2) + (p.y - r.y).powi(2) + (p.z - r.z).powi(2);
|
||||
assert!(gap2 <= d * d, "at {t}: {gap2} past {d}²");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curves_of_other_degrees_and_knots_are_compared_on_common_pieces() {
|
||||
// a line from 0 to 5 along x at speed 5, as a polyline of six points (knots at
|
||||
// fifths) and as one segment, and that segment raised to a cubic
|
||||
let polyline = spline(&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0].map(|x| [x, 0.0, 0.0]), 1);
|
||||
let line = spline(&[[0.0, 0.0, 0.0], [5.0, 0.0, 0.0]], 1);
|
||||
let d = certified_distance(&polyline, &line).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
let cubic = line.elevate_degree(2).unwrap();
|
||||
let d = certified_distance(&polyline, &cubic).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
// moved off the line by 2 in y at its middle: at least the gap there
|
||||
let bent = spline(&[[0.0, 0.0, 0.0], [2.5, 4.0, 0.0], [5.0, 0.0, 0.0]], 2);
|
||||
let d = certified_distance(&polyline, &bent).unwrap();
|
||||
assert!(d >= 2.0, "{d}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_periodic_curves_ends_are_read_through_the_general_basis() {
|
||||
// the periodic uniform cubic on a square's corners (polygon wrapped, knots unclamped)
|
||||
// starts and ends at (P0 + 4 P1 + P2) / 6; the NLib eval would answer P0 and P2
|
||||
let square = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
];
|
||||
let pw = square
|
||||
.iter()
|
||||
.chain(&square[..3])
|
||||
.map(|&[x, y, z]| CPoint::from_point(Vec3::new(x, y, z)))
|
||||
.collect();
|
||||
let knots = crate::nurbs::knots::periodic_uniform(4, 3).unwrap();
|
||||
let closed = Curve::new(pw, 3, knots).unwrap();
|
||||
let start = Vec3::new(50.0 / 6.0, 10.0 / 6.0, 0.0);
|
||||
let d = interpolation_bound(&closed, &[0.0, 1.0], &[start, start]).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_interpolants_bound_holds_its_misses_and_counts_them() {
|
||||
let c = spline(&WAVE, 3);
|
||||
let params = [0.0, 0.25, 0.5, 1.0];
|
||||
let on: Vec<Vec3> = params
|
||||
.iter()
|
||||
.map(|&u| c.eval(u, Side::Left).unwrap())
|
||||
.collect();
|
||||
let d = interpolation_bound(&c, ¶ms, &on).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
let mut off = on.clone();
|
||||
off[2].z += 0.5;
|
||||
let d = interpolation_bound(&c, ¶ms, &off).unwrap();
|
||||
assert!((0.5..0.5 + 1e-12).contains(&d), "{d}");
|
||||
assert!(interpolation_bound(&c, ¶ms[..3], &on).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_not_finite_are_refused_not_dropped() {
|
||||
let c = spline(&WAVE, 3);
|
||||
let params = [0.0, 0.5, 1.0];
|
||||
let mut on: Vec<Vec3> = params
|
||||
.iter()
|
||||
.map(|&u| c.eval(u, Side::Left).unwrap())
|
||||
.collect();
|
||||
assert!(matches!(
|
||||
interpolation_bound(&c, &[0.0, f64::NAN, 1.0], &on),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
on[1].y = f64::NAN;
|
||||
assert!(matches!(
|
||||
interpolation_bound(&c, ¶ms, &on),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
let mut bad = WAVE;
|
||||
bad[2][0] = f64::NAN;
|
||||
let nan = spline(&bad, 3);
|
||||
assert!(matches!(
|
||||
certified_distance(&c, &nan),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
interpolation_bound(&nan, ¶ms, &on[..]),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_extreme_degree_is_refused_not_overflowed() {
|
||||
let c = spline(&WAVE, 3);
|
||||
let pw = WAVE
|
||||
.iter()
|
||||
.map(|&[x, y, z]| CPoint::from_point(Vec3::new(x, y, z)))
|
||||
.collect();
|
||||
let wild = Curve::new(pw, usize::MAX, clamped_uniform(6, 3).unwrap()).unwrap();
|
||||
assert!(matches!(
|
||||
certified_distance(&c, &wild),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
certified_distance(&wild, &c),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
interpolation_bound(&wild, &[0.0], &[Vec3::new(0.0, 0.0, 0.0)]),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gap_near_the_bottom_of_the_floating_range_is_not_lost_to_underflow() {
|
||||
// a line of length 1e-200 and points 1e-200 off it: squaring 1e-200 underflows to 0
|
||||
let line = spline(&[[0.0, 0.0, 0.0], [1e-200, 0.0, 0.0]], 1);
|
||||
let off = Vec3::new(0.0, 1e-200, 0.0);
|
||||
let d = interpolation_bound(&line, &[0.0], &[off]).unwrap();
|
||||
assert!(d >= 1e-200, "{d}");
|
||||
let beside = spline(&[[0.0, 1e-200, 0.0], [1e-200, 1e-200, 0.0]], 1);
|
||||
let d = certified_distance(&line, &beside).unwrap();
|
||||
assert!(d >= 1e-200, "{d}");
|
||||
assert!(d <= 1.0001e-200, "{d}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bound_follows_the_curves_size_not_their_place() {
|
||||
// the wave a thousand kilometres along x, against itself and its refinement: the
|
||||
// rounding counted is the wave's, not 1e9's
|
||||
let far: Vec<[f64; 3]> = WAVE.iter().map(|&[x, y, z]| [x + 1e9, y, z]).collect();
|
||||
let c = spline(&far, 3);
|
||||
let d = certified_distance(&c, &c).unwrap();
|
||||
assert!(d < 1e-9, "{d}");
|
||||
let higher = c.elevate_degree(1).unwrap();
|
||||
let d = certified_distance(&c, &higher).unwrap();
|
||||
// the raise itself rounds at 1e9 (u 1e9 is about 1e-7): the bound holds that, no more
|
||||
assert!(d < 1e-5, "{d}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rational_or_other_domains_are_refused() {
|
||||
let c = spline(&WAVE, 3);
|
||||
let mut other = c.clone();
|
||||
other
|
||||
.reparam_to_interval(crate::nurbs::geom::Interval { ul: 0.0, ur: 2.0 })
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
certified_distance(&c, &other),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
let mut rational = c.clone();
|
||||
rational.non_rat_to_rat();
|
||||
assert!(matches!(
|
||||
certified_distance(&c, &rational),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
//! The Gordon surface of a network of curves: u-curves `C_k` (k = 0..=r) crossed by v-curves
|
||||
//! `D_l` (l = 0..=s), each u-curve crossing every v-curve once.
|
||||
//!
|
||||
//! Upstream: none (not upstream; solidcore-native). Sources: W. J. Gordon, "Spline-blended
|
||||
//! surface interpolation through curve networks", J. Math. Mech. 18 (1969); Piegl and Tiller,
|
||||
//! *The NURBS Book*, 2nd ed., §10.5 (Gordon surfaces), §9.2.1 (global interpolation) and
|
||||
//! eq. 9.8 (averaged knots).
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |------------|----------|
|
||||
//! | [`gordon`] | none |
|
||||
//!
|
||||
//! The surface is `S = L1 + L2 - T` (Gordon's Boolean sum): `L1` skins the u-curves through
|
||||
//! the v parameters `v_k` of their crossings, `L2` skins the v-curves through the u parameters
|
||||
//! `u_l`, and `T` is the tensor-product surface through the crossing points `Q_kl` at
|
||||
//! `(u_l, v_k)`. Each u-curve then lies on `S` at `v = v_k` and each v-curve at `u = u_l`
|
||||
//! provided the network is compatible: every v-curve crosses all the u-curves at one `u_l`
|
||||
//! of theirs, and every u-curve all the v-curves at one `v_k`. This module takes the compatible
|
||||
//! networks only: the crossings are found by the kernel's curve intersector
|
||||
//! ([`intersect_curves`]), `u_l` and `v_k` are their means (the outer curves' at the curves'
|
||||
//! ends), and every crossing is proved within the tolerance at those common parameters, else
|
||||
//! the network is refused naming the pair. The cross degrees are `min(3, count - 1)` with
|
||||
//! averaged knots; the three surfaces are made compatible (degree elevation, knot insertion)
|
||||
//! and their nets combined.
|
||||
//!
|
||||
//! The result is certified: the distance between each input curve and the surface's iso-curve
|
||||
//! at its parameter ([`certified_distance`], plus the rounding of extracting the iso-curve by
|
||||
//! knot insertion: `γ_(12 (d + 1))` of twice the net's largest coordinate per coordinate, `d`
|
||||
//! the cross degree, a coordinate error `e` moving a point by at most `2 e`), all counted in the
|
||||
//! network's own frame (minus its first control point, each moved coordinate rounding once) so
|
||||
//! the bound follows the network's size, not its distance from the origin; the largest such
|
||||
//! bound is returned with the surface. Past the tolerance the network is refused.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). Non-rational 3D curves only. No recursion.
|
||||
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use crate::curve::curve::Curve as KCurve;
|
||||
use crate::error::intersect::IntersectFailure;
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::intersect::api_intersectors::intersect_curves;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::limits::try_reserve_total;
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::nurbs::basis_basic::COMPATIBLE_KNOT_TOL;
|
||||
use crate::nurbs::crv_basic::Curve;
|
||||
use crate::nurbs::crv_deviation::{certified_distance, interpolation_bound};
|
||||
use crate::nurbs::crv_fit::p1::{interp_given_params, interp_knot_vector};
|
||||
use crate::nurbs::crv_tool::p2::make_compatible_knot_tol;
|
||||
use crate::nurbs::knots::{KnotVector, Side};
|
||||
use crate::nurbs::math::{gamma, CPoint};
|
||||
use crate::nurbs::srf_basic::{Direction, Surface};
|
||||
use crate::nurbs::srf_tool::p1::extract_iso_crv;
|
||||
use crate::nurbs::srf_tool::p4::make_compatible_uv;
|
||||
|
||||
const OP: &str = "nurbs::gordon::gordon";
|
||||
|
||||
/// The fewest curves of a network each way.
|
||||
pub const MIN_CURVES: usize = 2;
|
||||
/// The most curves of a network each way.
|
||||
pub const MAX_CURVES: usize = 64;
|
||||
|
||||
/// The highest cross degree (the degree across the curves of one family).
|
||||
const CROSS_DEGREE: usize = 3;
|
||||
|
||||
/// A Gordon surface and the bound it is certified to: no input curve is farther than `bound`
|
||||
/// from the surface's iso-curve at its parameter.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Gordon {
|
||||
/// The surface: u along the u-curves, v along the v-curves.
|
||||
pub surface: BSplineSurface,
|
||||
/// The largest certified distance between an input curve and its iso-curve.
|
||||
pub bound: f64,
|
||||
}
|
||||
|
||||
/// Why a network has no Gordon surface here.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum NetworkRefusal {
|
||||
/// A curve is rational: the Boolean sum of control nets is exact for polynomial curves
|
||||
/// only.
|
||||
Rational,
|
||||
/// u-curve `u` and v-curve `v` do not cross once: `count` crossings found.
|
||||
Crossings {
|
||||
/// The u-curve.
|
||||
u: usize,
|
||||
/// The v-curve.
|
||||
v: usize,
|
||||
/// How many crossings the intersector found.
|
||||
count: usize,
|
||||
},
|
||||
/// The crossings along one family do not increase from curve to curve (the curves are
|
||||
/// not in order, or do not run one way).
|
||||
Order,
|
||||
/// At the network's common parameters u-curve `u` and v-curve `v` are not proved within
|
||||
/// the tolerance: `gap` bounds how far apart they are there.
|
||||
Gap {
|
||||
/// The u-curve.
|
||||
u: usize,
|
||||
/// The v-curve.
|
||||
v: usize,
|
||||
/// The certified bound of their distance at the common parameters.
|
||||
gap: f64,
|
||||
},
|
||||
/// The surface is built but an input curve is not proved within the tolerance of it:
|
||||
/// `bound`, the largest certified distance.
|
||||
PastTolerance {
|
||||
/// The largest certified distance.
|
||||
bound: f64,
|
||||
},
|
||||
}
|
||||
|
||||
fn bad(reason: &'static str) -> SolidError {
|
||||
SolidError::invalid_input(OP, reason)
|
||||
}
|
||||
|
||||
/// `c`'s NURB, in polynomial form when its weights are all one value: a curve whose weights are
|
||||
/// equal is the polynomial curve on its points divided by that weight, exactly (a record
|
||||
/// spline's kernel curve carries a weight of 1 on every point). Only unequal weights make it
|
||||
/// rational.
|
||||
fn nurb_of(c: &BSplineCurve) -> Result<Curve> {
|
||||
let mut nurb = c
|
||||
.nurb
|
||||
.clone()
|
||||
.ok_or_else(|| bad("a curve without its NURB"))?;
|
||||
if nurb.is_rational() {
|
||||
let first = nurb.cpts().first().map(|p| p.w);
|
||||
if nurb.cpts().iter().all(|p| Some(p.w) == first) {
|
||||
nurb.make_non_rat();
|
||||
}
|
||||
}
|
||||
Ok(nurb)
|
||||
}
|
||||
|
||||
/// `c` as the kernel's curve, for the intersector.
|
||||
fn kernel_curve(c: &Curve) -> KCurve {
|
||||
KCurve::from(BSplineCurve::from_gw_nurb(3, c))
|
||||
}
|
||||
|
||||
/// The one crossing of `a` and `b` (its parameter on each), or how many there are.
|
||||
fn crossing(k: &Kernel, a: &Curve, b: &Curve) -> Result<std::result::Result<(f64, f64), usize>> {
|
||||
match intersect_curves(k, &kernel_curve(a), &kernel_curve(b), false) {
|
||||
Ok(found) => match (found.params1.as_slice(), found.params2.as_slice()) {
|
||||
([ta], [tb]) => Ok(Ok((*ta, *tb))),
|
||||
(all, _) => Ok(Err(all.len())),
|
||||
},
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.intersect_failure(),
|
||||
Some(IntersectFailure::Failed("the curves do not intersect"))
|
||||
) =>
|
||||
{
|
||||
Ok(Err(0))
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `(x, y, z)` of a polynomial control point.
|
||||
fn point(c: CPoint) -> Vec3 {
|
||||
Vec3::new(c.x, c.y, c.z)
|
||||
}
|
||||
|
||||
/// The curve of degree `p` through `points` at `params` on `knots` (global interpolation).
|
||||
fn through(points: &[Vec3], params: &[f64], knots: &[f64], p: usize) -> Result<Curve> {
|
||||
interp_given_params(points, params, &KnotVector::from_slice(knots)?, p)
|
||||
}
|
||||
|
||||
/// Whether `t` increases strictly.
|
||||
fn increasing(t: &[f64]) -> bool {
|
||||
t.windows(2).all(|w| matches!(w, [a, b] if a < b))
|
||||
}
|
||||
|
||||
/// The surface whose net is `net[i][j]` (`i` along u), degrees `p`, `q`, knots `u`, `v`.
|
||||
fn surface(net: Vec<Vec<Vec3>>, p: usize, q: usize, u: &[f64], v: &[f64]) -> Result<Surface> {
|
||||
let net = net
|
||||
.into_iter()
|
||||
.map(|row| row.into_iter().map(CPoint::from_point).collect())
|
||||
.collect();
|
||||
Surface::new(net, p, q, u.to_vec(), v.to_vec())
|
||||
}
|
||||
|
||||
/// `c` moved by minus `o`, into the network's own frame.
|
||||
fn moved(c: &Curve, o: CPoint) -> Result<Curve> {
|
||||
let pw = c
|
||||
.cpts()
|
||||
.iter()
|
||||
.map(|q| CPoint::new(q.x - o.x, q.y - o.y, q.z - o.z, q.w))
|
||||
.collect();
|
||||
Curve::new(pw, c.degree(), c.knots().to_vec())
|
||||
}
|
||||
|
||||
/// `s` moved by minus `o`, into the network's own frame.
|
||||
fn moved_surface(s: &Surface, o: CPoint) -> Result<Surface> {
|
||||
let net = s
|
||||
.rows()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|q| CPoint::new(q.x - o.x, q.y - o.y, q.z - o.z, q.w))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let (p, q) = s.degrees();
|
||||
Surface::new(net, p, q, s.u_knots().to_vec(), s.v_knots().to_vec())
|
||||
}
|
||||
|
||||
/// The largest coordinate of `points`.
|
||||
fn largest<'a>(points: impl Iterator<Item = &'a CPoint>) -> f64 {
|
||||
points
|
||||
.map(|c| c.x.abs().max(c.y.abs()).max(c.z.abs()))
|
||||
.fold(0.0_f64, f64::max)
|
||||
}
|
||||
|
||||
/// An empty `Vec` with room for `count + 1` values, reserved fallibly: a count past the
|
||||
/// addressable or the allocation bound ([`try_reserve_total`]) is refused.
|
||||
fn room<T>(count: usize) -> Result<Vec<T>> {
|
||||
let len = count
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| bad("a count past the addressable"))?;
|
||||
let mut v = Vec::new();
|
||||
if !try_reserve_total(&mut v, len) {
|
||||
return Err(bad("an allocation past the bound"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Whether `x` is at most `tol`; a NaN is not.
|
||||
fn within(x: f64, tol: f64) -> bool {
|
||||
matches!(x.partial_cmp(&tol), Some(Ordering::Less | Ordering::Equal))
|
||||
}
|
||||
|
||||
/// The Gordon surface of the u-curves `us` and the v-curves `vs` (module docs), its crossings
|
||||
/// and its curves proved within `tol` of it.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for fewer than [`MIN_CURVES`] or more than [`MAX_CURVES`] curves a family, a
|
||||
/// curve without its NURB or `tol` not finite and positive; those of the intersector, the
|
||||
/// interpolation, the compatibility and the certificates.
|
||||
pub fn gordon(
|
||||
k: &Kernel,
|
||||
us: &[BSplineCurve],
|
||||
vs: &[BSplineCurve],
|
||||
tol: f64,
|
||||
) -> Result<std::result::Result<Gordon, NetworkRefusal>> {
|
||||
let family = MIN_CURVES..=MAX_CURVES;
|
||||
if !family.contains(&us.len()) || !family.contains(&vs.len()) {
|
||||
return Err(bad("a family of fewer than two or more than 64 curves"));
|
||||
}
|
||||
if !(tol.is_finite() && tol > 0.0) {
|
||||
return Err(bad("a tolerance not finite and positive"));
|
||||
}
|
||||
let mut cu: Vec<Curve> = us.iter().map(nurb_of).collect::<Result<_>>()?;
|
||||
let mut dv: Vec<Curve> = vs.iter().map(nurb_of).collect::<Result<_>>()?;
|
||||
if cu.iter().chain(&dv).any(Curve::is_rational) {
|
||||
return Ok(Err(NetworkRefusal::Rational));
|
||||
}
|
||||
// each family on one degree, knot vector and domain, exactly
|
||||
make_compatible_knot_tol(&mut cu, COMPATIBLE_KNOT_TOL)?;
|
||||
make_compatible_knot_tol(&mut dv, COMPATIBLE_KNOT_TOL)?;
|
||||
let (r, s) = (cu.len() - 1, dv.len() - 1);
|
||||
|
||||
// the crossings: on u-curve kk at u_cross[kk][l], on v-curve l at v_cross[kk][l]
|
||||
let mut u_cross = vec![vec![0.0; s + 1]; r + 1];
|
||||
let mut v_cross = vec![vec![0.0; s + 1]; r + 1];
|
||||
for (kk, c) in cu.iter().enumerate() {
|
||||
for (l, d) in dv.iter().enumerate() {
|
||||
match crossing(k, c, d)? {
|
||||
Ok((tu, tv)) => {
|
||||
if let (Some(a), Some(b)) = (
|
||||
u_cross.get_mut(kk).and_then(|row| row.get_mut(l)),
|
||||
v_cross.get_mut(kk).and_then(|row| row.get_mut(l)),
|
||||
) {
|
||||
*a = tu;
|
||||
*b = tv;
|
||||
}
|
||||
}
|
||||
Err(count) => {
|
||||
return Ok(Err(NetworkRefusal::Crossings { u: kk, v: l, count }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// the common parameters: the means, the outer curves' at the families' ends
|
||||
let mut u_at: Vec<f64> = (0..=s)
|
||||
.map(|l| u_cross.iter().filter_map(|row| row.get(l)).sum::<f64>() / (r + 1) as f64)
|
||||
.collect();
|
||||
let mut v_at: Vec<f64> = (0..=r)
|
||||
.map(|kk| v_cross.get(kk).map_or(0.0, |row| row.iter().sum::<f64>()) / (s + 1) as f64)
|
||||
.collect();
|
||||
let (Some(c0), Some(d0)) = (cu.first(), dv.first()) else {
|
||||
return Err(bad("an empty family"));
|
||||
};
|
||||
// in order: each family's crossings increase from curve to curve
|
||||
if !increasing(&u_at) || !increasing(&v_at) {
|
||||
return Ok(Err(NetworkRefusal::Order));
|
||||
}
|
||||
let ((ua, ub), (va, vb)) = (c0.param_bounds(), d0.param_bounds());
|
||||
if let Some(first) = u_at.first_mut() {
|
||||
*first = ua;
|
||||
}
|
||||
if let Some(last) = u_at.last_mut() {
|
||||
*last = ub;
|
||||
}
|
||||
if let Some(first) = v_at.first_mut() {
|
||||
*first = va;
|
||||
}
|
||||
if let Some(last) = v_at.last_mut() {
|
||||
*last = vb;
|
||||
}
|
||||
if !increasing(&u_at) || !increasing(&v_at) {
|
||||
return Ok(Err(NetworkRefusal::Order));
|
||||
}
|
||||
|
||||
// The certificates are counted in the network's own frame (minus the first u-curve's
|
||||
// first control point), so their rounding follows the network's size, not its distance
|
||||
// from the origin: each moved coordinate rounds once (`γ_1` of the moved value), moving a
|
||||
// point by at most `2 γ_1 M`, `M` the largest moved coordinate.
|
||||
let origin = c0
|
||||
.cpts()
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or_else(|| bad("a curve without points"))?;
|
||||
let cu_local: Vec<Curve> = cu.iter().map(|c| moved(c, origin)).collect::<Result<_>>()?;
|
||||
let dv_local: Vec<Curve> = dv.iter().map(|d| moved(d, origin)).collect::<Result<_>>()?;
|
||||
let size = largest(cu_local.iter().chain(&dv_local).flat_map(Curve::cpts));
|
||||
let move_err = 2.0 * gamma(1) * size;
|
||||
|
||||
// the crossing points at the common parameters, each proved within tol of both curves:
|
||||
// |C(u) - D(v)| <= |C(u) - q| + |D(v) - q| for any q, both bounded in the network's frame
|
||||
let mut grid: Vec<Vec<Vec3>> = room(r)?;
|
||||
for (kk, ((c, c_local), &v)) in cu.iter().zip(&cu_local).zip(&v_at).enumerate() {
|
||||
let mut row = room(s)?;
|
||||
for (l, (d_local, &u)) in dv_local.iter().zip(&u_at).enumerate() {
|
||||
let q = c.eval(u, Side::Left)?;
|
||||
let q_local = c_local.eval(u, Side::Left)?;
|
||||
let on_c = interpolation_bound(c_local, &[u], &[q_local])?;
|
||||
let on_d = interpolation_bound(d_local, &[v], &[q_local])?;
|
||||
let gap = (on_c + on_d + 2.0 * move_err) * (1.0 + gamma(2));
|
||||
if !within(gap, tol) {
|
||||
return Ok(Err(NetworkRefusal::Gap { u: kk, v: l, gap }));
|
||||
}
|
||||
row.push(q);
|
||||
}
|
||||
grid.push(row);
|
||||
}
|
||||
|
||||
let (pu, qv) = (s.min(CROSS_DEGREE), r.min(CROSS_DEGREE));
|
||||
let u_knots = interp_knot_vector(&u_at, pu)?;
|
||||
let v_knots = interp_knot_vector(&v_at, qv)?;
|
||||
|
||||
// L1: the u-curves skinned in v, column by column of their (common) control points
|
||||
let (p, cu_knots) = (c0.degree(), c0.knots().to_vec());
|
||||
let n = c0.n();
|
||||
let mut l1 = room(n)?;
|
||||
for i in 0..=n {
|
||||
let column: Vec<Vec3> = cu
|
||||
.iter()
|
||||
.map(|c| c.cpts().get(i).copied().map(point))
|
||||
.collect::<Option<_>>()
|
||||
.ok_or_else(|| bad("curves of one family with different nets"))?;
|
||||
let skin = through(&column, &v_at, &v_knots, qv)?;
|
||||
l1.push(skin.cpts().iter().copied().map(point).collect());
|
||||
}
|
||||
let l1 = surface(l1, p, qv, &cu_knots, &v_knots)?;
|
||||
|
||||
// L2: the v-curves skinned in u, row by row of their control points
|
||||
let (q, dv_knots) = (d0.degree(), d0.knots().to_vec());
|
||||
let m = d0.n();
|
||||
let mut by_j: Vec<Vec<Vec3>> = room(m)?;
|
||||
for j in 0..=m {
|
||||
let column: Vec<Vec3> = dv
|
||||
.iter()
|
||||
.map(|d| d.cpts().get(j).copied().map(point))
|
||||
.collect::<Option<_>>()
|
||||
.ok_or_else(|| bad("curves of one family with different nets"))?;
|
||||
let skin = through(&column, &u_at, &u_knots, pu)?;
|
||||
by_j.push(skin.cpts().iter().copied().map(point).collect());
|
||||
}
|
||||
let l2_net: Vec<Vec<Vec3>> = (0..=s)
|
||||
.map(|i| by_j.iter().filter_map(|col| col.get(i).copied()).collect())
|
||||
.collect();
|
||||
let l2 = surface(l2_net, pu, q, &u_knots, &dv_knots)?;
|
||||
|
||||
// T: the crossing points interpolated, along u then along v
|
||||
let mut along_u: Vec<Vec<Vec3>> = room(r)?;
|
||||
for row in &grid {
|
||||
let c = through(row, &u_at, &u_knots, pu)?;
|
||||
along_u.push(c.cpts().iter().copied().map(point).collect());
|
||||
}
|
||||
let mut t_net: Vec<Vec<Vec3>> = room(s)?;
|
||||
for i in 0..=s {
|
||||
let column: Vec<Vec3> = along_u
|
||||
.iter()
|
||||
.filter_map(|row| row.get(i).copied())
|
||||
.collect();
|
||||
let c = through(&column, &v_at, &v_knots, qv)?;
|
||||
t_net.push(c.cpts().iter().copied().map(point).collect());
|
||||
}
|
||||
let t = surface(t_net, pu, qv, &u_knots, &v_knots)?;
|
||||
|
||||
// S = L1 + L2 - T on one degree and knot vector each way
|
||||
let mut three = [l1, l2, t];
|
||||
make_compatible_uv(&mut three)?;
|
||||
let [l1, l2, t] = &three;
|
||||
let (nn, mm) = (l1.n(), l1.m());
|
||||
let mut net = room(nn)?;
|
||||
for i in 0..=nn {
|
||||
let mut row = room(mm)?;
|
||||
for j in 0..=mm {
|
||||
let (a, b, c) = (
|
||||
point(l1.cpt(i, j)?),
|
||||
point(l2.cpt(i, j)?),
|
||||
point(t.cpt(i, j)?),
|
||||
);
|
||||
row.push(Vec3::new(a.x + b.x - c.x, a.y + b.y - c.y, a.z + b.z - c.z));
|
||||
}
|
||||
net.push(row);
|
||||
}
|
||||
let (sp, sq) = l1.degrees();
|
||||
let summed = surface(net, sp, sq, l1.u_knots(), l1.v_knots())?;
|
||||
let made = BSplineSurface::from_gw_nurb(&summed);
|
||||
let s_nurb = made
|
||||
.nurb
|
||||
.as_ref()
|
||||
.ok_or_else(|| bad("a surface without its NURB"))?;
|
||||
|
||||
// the certificate: each input curve against its iso-curve, in the network's frame; the
|
||||
// extraction's rounding and both moves included
|
||||
let s_local = moved_surface(s_nurb, origin)?;
|
||||
let s_size = largest(s_local.rows().flatten()).max(size);
|
||||
let s_move = 2.0 * gamma(1) * s_size;
|
||||
let (dp, dq) = s_local.degrees();
|
||||
let mut bound = 0.0_f64;
|
||||
for (c, &v) in cu_local.iter().zip(&v_at) {
|
||||
let iso = extract_iso_crv(&s_local, v, Direction::U)?;
|
||||
let extraction = 4.0 * gamma(12 * (dq + 1)) * s_size;
|
||||
let d = (certified_distance(&iso, c)? + extraction + s_move + move_err) * (1.0 + gamma(3));
|
||||
bound = bound.max(d);
|
||||
}
|
||||
for (d, &u) in dv_local.iter().zip(&u_at) {
|
||||
let iso = extract_iso_crv(&s_local, u, Direction::V)?;
|
||||
let extraction = 4.0 * gamma(12 * (dp + 1)) * s_size;
|
||||
let e = (certified_distance(&iso, d)? + extraction + s_move + move_err) * (1.0 + gamma(3));
|
||||
bound = bound.max(e);
|
||||
}
|
||||
if !within(bound, tol) {
|
||||
return Ok(Err(NetworkRefusal::PastTolerance { bound }));
|
||||
}
|
||||
Ok(Ok(Gordon {
|
||||
surface: made,
|
||||
bound,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The polynomial curve of degree `p` on `points` with the clamped uniform knots.
|
||||
fn spline(points: &[[f64; 3]], p: usize) -> BSplineCurve {
|
||||
let n = points.len();
|
||||
let mut knots = vec![0.0; p + 1];
|
||||
for i in 1..n - p {
|
||||
knots.push(i as f64 / (n - p) as f64);
|
||||
}
|
||||
knots.extend(vec![1.0; p + 1]);
|
||||
let pw = points
|
||||
.iter()
|
||||
.map(|q| CPoint::from_point(Vec3::new(q[0], q[1], q[2])))
|
||||
.collect();
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, p, knots).unwrap())
|
||||
}
|
||||
|
||||
fn line(a: [f64; 3], b: [f64; 3]) -> BSplineCurve {
|
||||
spline(&[a, b], 1)
|
||||
}
|
||||
|
||||
fn at(s: &Surface, u: f64, v: f64) -> Vec3 {
|
||||
let iso = extract_iso_crv(s, v, Direction::U).unwrap();
|
||||
iso.eval(u, Side::Left).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_network_of_lines_is_the_bilinear_patch() {
|
||||
let k = Kernel::default();
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 5.0],
|
||||
];
|
||||
let us = [line(p[0], p[1]), line(p[2], p[3])];
|
||||
let vs = [line(p[0], p[2]), line(p[1], p[3])];
|
||||
let g = gordon(&k, &us, &vs, 1e-6).unwrap().unwrap();
|
||||
assert!(g.bound <= 1e-9, "{}", g.bound);
|
||||
let s = g.surface.nurb.as_ref().unwrap();
|
||||
for (a, b) in [(0.0, 0.0), (0.3, 0.7), (1.0, 0.5), (0.5, 1.0)] {
|
||||
let bilinear = |i: usize| {
|
||||
(1.0 - b) * ((1.0 - a) * p[0][i] + a * p[1][i])
|
||||
+ b * ((1.0 - a) * p[2][i] + a * p[3][i])
|
||||
};
|
||||
let q = at(s, a, b);
|
||||
assert!(
|
||||
(q.x - bilinear(0)).abs() < 1e-9
|
||||
&& (q.y - bilinear(1)).abs() < 1e-9
|
||||
&& (q.z - bilinear(2)).abs() < 1e-9,
|
||||
"{q:?} at ({a}, {b})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_curved_network_holds_its_curves() {
|
||||
let k = Kernel::default();
|
||||
// three u-curves and three v-curves of the saddle z = (x - 5)(y - 5) / 10, crossing at
|
||||
// their middles (its sections along x and y are lines: each curve is exact)
|
||||
let z = |x: f64, y: f64| (x - 5.0) * (y - 5.0) / 10.0;
|
||||
let u_curve = |y: f64| {
|
||||
spline(
|
||||
&[
|
||||
[0.0, y, z(0.0, y)],
|
||||
[5.0, y, z(5.0, y)],
|
||||
[10.0, y, z(10.0, y)],
|
||||
],
|
||||
1,
|
||||
)
|
||||
};
|
||||
let v_curve = |x: f64| {
|
||||
spline(
|
||||
&[
|
||||
[x, 0.0, z(x, 0.0)],
|
||||
[x, 5.0, z(x, 5.0)],
|
||||
[x, 10.0, z(x, 10.0)],
|
||||
],
|
||||
1,
|
||||
)
|
||||
};
|
||||
let us = [u_curve(0.0), u_curve(5.0), u_curve(10.0)];
|
||||
let vs = [v_curve(0.0), v_curve(5.0), v_curve(10.0)];
|
||||
let g = gordon(&k, &us, &vs, 1e-6).unwrap().unwrap();
|
||||
assert!(g.bound <= 1e-6, "{}", g.bound);
|
||||
// every input curve on the surface at its parameter, certified
|
||||
let s = g.surface.nurb.as_ref().unwrap();
|
||||
for (c, v) in us.iter().zip([0.0, 0.5, 1.0]) {
|
||||
let iso = extract_iso_crv(s, v, Direction::U).unwrap();
|
||||
assert!(certified_distance(&iso, c.nurb.as_ref().unwrap()).unwrap() <= 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_networks_are_refused() {
|
||||
let k = Kernel::default();
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 0.0],
|
||||
];
|
||||
// the second v-curve stops short of the second u-curve: no crossing
|
||||
let us = [line(p[0], p[1]), line(p[2], p[3])];
|
||||
let vs = [line(p[0], p[2]), line(p[1], [10.0, 5.0, 0.0])];
|
||||
assert_eq!(
|
||||
gordon(&k, &us, &vs, 1e-6).unwrap().unwrap_err(),
|
||||
NetworkRefusal::Crossings {
|
||||
u: 1,
|
||||
v: 1,
|
||||
count: 0
|
||||
}
|
||||
);
|
||||
// the u-curves listed out of order
|
||||
let vs = [line(p[0], p[2]), line(p[1], p[3])];
|
||||
let reversed = [line(p[2], p[3]), line(p[0], p[1])];
|
||||
assert_eq!(
|
||||
gordon(&k, &reversed, &vs, 1e-6).unwrap().unwrap_err(),
|
||||
NetworkRefusal::Order
|
||||
);
|
||||
// a v-curve crossing the u-curves at different parameters: a gap at the common ones
|
||||
let us = [line(p[0], p[1]), line(p[2], p[3])];
|
||||
let slanted = [line(p[0], p[2]), line([10.0, 0.0, 0.0], [6.0, 10.0, 0.0])];
|
||||
assert!(matches!(
|
||||
gordon(&k, &us, &slanted, 1e-6).unwrap().unwrap_err(),
|
||||
NetworkRefusal::Gap { .. } | NetworkRefusal::Crossings { .. }
|
||||
));
|
||||
// too few curves
|
||||
assert!(gordon(&k, &us[..1], &vs, 1e-6).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_patchs_iso_curves_give_the_patch_back() {
|
||||
// a polynomial patch, cubic in u with two interior knots and a cubic Bezier in v: four
|
||||
// u-curves skinned by cubics reproduce its v, and L2 = T exactly, so S is the patch
|
||||
let k = Kernel::default();
|
||||
let net: Vec<Vec<CPoint>> = (0..6)
|
||||
.map(|i| {
|
||||
(0..4)
|
||||
.map(|j| {
|
||||
let z = f64::from((i * 7 + j * 3) % 5) - 2.0;
|
||||
CPoint::from_point(Vec3::new(2.0 * f64::from(i), 3.0 * f64::from(j), z))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let third = 1.0 / 3.0;
|
||||
let patch = Surface::new(
|
||||
net,
|
||||
3,
|
||||
3,
|
||||
vec![0.0, 0.0, 0.0, 0.0, third, 2.0 * third, 1.0, 1.0, 1.0, 1.0],
|
||||
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||
)
|
||||
.unwrap();
|
||||
let iso = |t: f64, dir: Direction| {
|
||||
BSplineCurve::from_gw_nurb(3, &extract_iso_crv(&patch, t, dir).unwrap())
|
||||
};
|
||||
let us: Vec<BSplineCurve> = [0.0, 0.3, 0.7, 1.0].map(|v| iso(v, Direction::U)).to_vec();
|
||||
let vs: Vec<BSplineCurve> = [0.0, 0.25, 0.6, 1.0].map(|u| iso(u, Direction::V)).to_vec();
|
||||
let g = gordon(&k, &us, &vs, 1e-6).unwrap().unwrap();
|
||||
assert!(g.bound <= 1e-9, "{}", g.bound);
|
||||
let s = g.surface.nurb.as_ref().unwrap();
|
||||
for i in 0..=10 {
|
||||
for j in 0..=10 {
|
||||
let (a, b) = (f64::from(i) / 10.0, f64::from(j) / 10.0);
|
||||
let (p, q) = (at(s, a, b), at(&patch, a, b));
|
||||
assert!((p - q).length() < 1e-9, "{p:?} against {q:?} at ({a}, {b})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_grid_meets_at_its_corners_once() {
|
||||
// three lines each way on the rectangle [0, 10] x [0, 20]: the outer curves meet at
|
||||
// their ends, and the inner ones end on the outer ones
|
||||
let k = Kernel::default();
|
||||
let us: Vec<BSplineCurve> = [0.0, 5.0, 20.0]
|
||||
.map(|y| line([0.0, y, 0.0], [10.0, y, 0.0]))
|
||||
.to_vec();
|
||||
let vs: Vec<BSplineCurve> = [0.0, 4.0, 10.0]
|
||||
.map(|x| line([x, 0.0, 0.0], [x, 20.0, 0.0]))
|
||||
.to_vec();
|
||||
let g = gordon(&k, &us, &vs, 1e-6).unwrap().unwrap();
|
||||
assert!(g.bound <= 1e-9, "{}", g.bound);
|
||||
let s = g.surface.nurb.as_ref().unwrap();
|
||||
for (a, b) in [(0.0, 0.0), (1.0, 1.0), (0.4, 0.25), (0.7, 0.9)] {
|
||||
let p = at(s, a, b);
|
||||
assert!(p.z.abs() < 1e-9 && (0.0..=10.0).contains(&p.x) && (0.0..=20.0).contains(&p.y));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bound_follows_the_networks_size_far_from_the_origin() {
|
||||
// the bilinear network far out: its bound follows its own size, not its coordinates
|
||||
let k = Kernel::default();
|
||||
// 2e5 out: counted on the coordinates, the extraction's rounding alone would be
|
||||
// 4 γ_24 2e5 ≈ 2e-9; on the network's size (10) it is below 1e-13, and the nets' own
|
||||
// rounding there about 1e-10
|
||||
let o = [1.0e5, -2.0e5, 3.0e4];
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 5.0],
|
||||
]
|
||||
.map(|q| [q[0] + o[0], q[1] + o[1], q[2] + o[2]]);
|
||||
let us = [line(p[0], p[1]), line(p[2], p[3])];
|
||||
let vs = [line(p[0], p[2]), line(p[1], p[3])];
|
||||
let g = gordon(&k, &us, &vs, 1e-6).unwrap().unwrap();
|
||||
assert!(g.bound <= 5e-10, "{}", g.bound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curves_weighted_one_throughout_are_polynomial() {
|
||||
// a record spline's kernel curve carries w = 1 on every point: not rational
|
||||
let k = Kernel::default();
|
||||
let weighted = |a: [f64; 3], b: [f64; 3]| {
|
||||
let pw = [a, b]
|
||||
.iter()
|
||||
.map(|q| CPoint::new(q[0], q[1], q[2], 1.0))
|
||||
.collect();
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, 1, vec![0.0, 0.0, 1.0, 1.0]).unwrap())
|
||||
};
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[10.0, 10.0, 5.0],
|
||||
];
|
||||
let us = [weighted(p[0], p[1]), weighted(p[2], p[3])];
|
||||
let vs = [line(p[0], p[2]), line(p[1], p[3])];
|
||||
let g = gordon(&k, &us, &vs, 1e-6).unwrap().unwrap();
|
||||
assert!(g.bound <= 1e-9, "{}", g.bound);
|
||||
// unequal weights are rational, and refused
|
||||
let pw = vec![
|
||||
CPoint::new(0.0, 0.0, 0.0, 1.0),
|
||||
CPoint::new(10.0, 0.0, 0.0, 2.0),
|
||||
];
|
||||
let rational =
|
||||
BSplineCurve::from_gw_nurb(3, &Curve::new(pw, 1, vec![0.0, 0.0, 1.0, 1.0]).unwrap());
|
||||
assert_eq!(
|
||||
gordon(&k, &[rational, us[1].clone()], &vs, 1e-6)
|
||||
.unwrap()
|
||||
.unwrap_err(),
|
||||
NetworkRefusal::Rational
|
||||
);
|
||||
}
|
||||
}
|
||||
+124
@@ -24,6 +24,8 @@
|
||||
//! | [`Side`] | `NL_LEFT` / `NL_RIGHT` flags |
|
||||
//! | [`MAX_DEGREE`] | `NL_MAXDEG` |
|
||||
//! | [`clamped_uniform`] | none (not upstream) |
|
||||
//! | [`periodic_uniform`] | none (not upstream) |
|
||||
//! | [`is_periodic`] | none (not upstream) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - Out-params become return values ([`Basis`], tuples, `Vec`s sized by the callee).
|
||||
@@ -844,9 +846,131 @@ pub fn clamped_uniform(count: usize, degree: usize) -> Result<Vec<f64>> {
|
||||
Ok(knots)
|
||||
}
|
||||
|
||||
/// The uniform knots of a closed (periodic) curve of `count` control points of `degree`,
|
||||
/// whose control polygon repeats its first `degree` points after its last (`count + degree`
|
||||
/// points in all): `count + 2 degree + 1` knots `(i - degree) / count`, evenly spaced, so the
|
||||
/// curve's domain `[knots[degree], knots[count + degree]]` is `[0, 1]` and it joins itself
|
||||
/// there with continuity `degree - 1` (Piegl and Tiller, *The NURBS Book*, 2nd ed., §12.2:
|
||||
/// a periodic, uniform knot vector, unclamped).
|
||||
///
|
||||
/// Not a clamped vector, so not one for the routines that take only those:
|
||||
/// [`KnotVector::validate`] (upstream's clamped check) refuses it ([`is_periodic`] is its
|
||||
/// check), and [`KnotVector::basis_eval`] (upstream's `N_BasisEval`, kept for parity) reads the
|
||||
/// domain's ends as clamped ones, so the NLib curve's `eval` is wrong at them: evaluate such a
|
||||
/// curve with the general basis ([`crate::eval`]'s evaluator, or the NLib curve's `derivs`).
|
||||
///
|
||||
/// Not upstream: the knots of a closed curve made from control points alone.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::InvalidInput`] for a degree of 0 or past [`MAX_DEGREE`], fewer than
|
||||
/// `degree + 1` points, or more knots than [`crate::limits::MAX_ALLOC_BYTES`] holds;
|
||||
/// [`NurbsFailure::Memory`] when they cannot be reserved.
|
||||
pub fn periodic_uniform(count: usize, degree: usize) -> Result<Vec<f64>> {
|
||||
if degree == 0 || degree > MAX_DEGREE || count < degree + 1 {
|
||||
return Err(SolidError::invalid_input(
|
||||
"nurbs::knots::periodic_uniform",
|
||||
"a degree of 0 or past the most, or too few points for it",
|
||||
));
|
||||
}
|
||||
let len = count
|
||||
.checked_add(2 * degree + 1)
|
||||
.filter(|&len| crate::limits::fits::<f64>(len))
|
||||
.ok_or_else(|| {
|
||||
SolidError::invalid_input("nurbs::knots::periodic_uniform", "too many points")
|
||||
})?;
|
||||
let mut knots = Vec::new();
|
||||
if !crate::limits::try_reserve_total(&mut knots, len) {
|
||||
return Err(SolidError::nurbs(
|
||||
"nurbs::knots::periodic_uniform",
|
||||
NurbsFailure::Memory,
|
||||
));
|
||||
}
|
||||
let n = count as f64;
|
||||
let p = degree as f64;
|
||||
knots.extend((0..len).map(|i| (i as f64 - p) / n));
|
||||
Ok(knots)
|
||||
}
|
||||
|
||||
/// Whether `knots` are a periodic curve's of `degree`: unclamped (`U[0] < U[p]`), and every
|
||||
/// knot `n` places on, `n = len - 2 degree - 1` the curve's distinct control points, one
|
||||
/// period `T = U[n + p] - U[p]` (the domain's length) past it: `U[i + n] - U[i] = T` for
|
||||
/// `i = 0..=2 degree`, within the rounding of knots computed on the domain (8 ε of the
|
||||
/// largest knot and the period). Then a polygon that repeats its first `degree` points after
|
||||
/// its last closes on itself with continuity `degree - 1` (Piegl and Tiller, *The NURBS Book*,
|
||||
/// 2nd ed., §12.2); repeated points on other knots do not.
|
||||
///
|
||||
/// Not upstream.
|
||||
#[must_use]
|
||||
pub fn is_periodic(knots: &[f64], degree: usize) -> bool {
|
||||
if degree == 0 || degree > MAX_DEGREE || !knots.iter().all(|k| k.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
let Some(n) = knots.len().checked_sub(2 * degree + 1).filter(|&n| n > 0) else {
|
||||
return false;
|
||||
};
|
||||
let (Some(&u0), Some(&up), Some(&unp)) =
|
||||
(knots.first(), knots.get(degree), knots.get(n + degree))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let period = unp - up;
|
||||
if !(u0 < up && period > 0.0) {
|
||||
return false;
|
||||
}
|
||||
let largest = knots.iter().fold(0.0_f64, |m, k| m.max(k.abs()));
|
||||
let slack = 8.0 * f64::EPSILON * (largest + period);
|
||||
(0..=2 * degree).all(|i| match (knots.get(i), knots.get(i + n)) {
|
||||
(Some(&a), Some(&b)) => (b - a - period).abs() <= slack,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn periodic_knots_repeat_their_spans_one_period_on() {
|
||||
for (count, degree) in [(3, 1), (4, 3), (7, 2), (12, 5)] {
|
||||
let knots = periodic_uniform(count, degree).unwrap();
|
||||
assert!(is_periodic(&knots, degree), "{count}, {degree}");
|
||||
// shifted and scaled, still periodic
|
||||
let moved: Vec<f64> = knots.iter().map(|k| 3.0 * k - 7.0).collect();
|
||||
assert!(is_periodic(&moved, degree), "{count}, {degree}");
|
||||
}
|
||||
// unclamped but not periodic: the spans past the domain differ from those in it
|
||||
assert!(!is_periodic(&[-2.0, -1.0, 0.0, 1.0, 2.0, 4.0, 5.0, 6.0], 2));
|
||||
// clamped, or too few knots for the degree, or not finite
|
||||
assert!(!is_periodic(&[0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0], 2));
|
||||
assert!(!is_periodic(&[0.0, 1.0, 2.0, 3.0], 2));
|
||||
assert!(!is_periodic(
|
||||
&[0.0, f64::NAN, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
|
||||
2
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn periodic_uniform_knots_are_evenly_spaced_with_the_domain_zero_to_one() {
|
||||
let knots = periodic_uniform(4, 3).unwrap();
|
||||
assert_eq!(knots.len(), 4 + 2 * 3 + 1);
|
||||
assert_eq!(knots[3], 0.0);
|
||||
assert_eq!(knots[7], 1.0);
|
||||
assert_eq!(knots[0], -0.75);
|
||||
assert_eq!(knots[10], 1.75);
|
||||
for w in knots.windows(2) {
|
||||
assert!((w[1] - w[0] - 0.25).abs() < 1e-15, "{knots:?}");
|
||||
}
|
||||
// the periodic contract, which the clamped validator (rightly) refuses
|
||||
assert!(is_periodic(&knots, 3));
|
||||
assert!(KnotVector::from_slice(&knots).unwrap().validate(3).is_err());
|
||||
for (count, degree) in [(3, 3), (5, 0), (1, 1), (usize::MAX / 2, 1), (usize::MAX, 3)] {
|
||||
assert!(
|
||||
periodic_uniform(count, degree).is_err(),
|
||||
"{count}, {degree}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamped_uniform_knots_clamp_both_ends_and_space_the_rest_evenly() {
|
||||
assert_eq!(
|
||||
|
||||
+361
@@ -32,8 +32,12 @@
|
||||
//! | [`basis_find_global_max`] | `sm_BasisFindGlobalMax` |
|
||||
//! | [`is_ncrv_degenerate`] | `sm_IsNCrvDegenerate` |
|
||||
//! | [`MAX_DEGREE`], [`MAX_DERIV`] | `GW_MAX_DEGREE`, `GW_MAX_DERIV` |
|
||||
//! | [`crv_span_boxes`] | none (not upstream) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - [`crv_span_boxes`] and its helpers are not upstream: the boxes of the control
|
||||
//! points of every knot span, a certified cover of the curve used to prune coincidence
|
||||
//! queries.
|
||||
//! - Out-params become return values. Routines that fill caller-allocated curves
|
||||
//! (`sm_CreateBezSegments`, `sm_CrvSplit`, `sm_CrvTrim` with `pExisting*`) allocate
|
||||
//! their outputs with exactly the sizes upstream checks for, so the size-mismatch
|
||||
@@ -2100,6 +2104,252 @@ pub fn is_ncrv_degenerate(cur: &Curve, tol3d: f64) -> Result<bool> {
|
||||
Ok(bx.is_point_sized(tol))
|
||||
}
|
||||
|
||||
/// The Euclidean point of a control point whose convex-hull property is certain:
|
||||
/// `None` for a 2D point, a non-finite coordinate, or a rational point whose weight is
|
||||
/// not finite and positive (a non-positive weight breaks the hull property, and the
|
||||
/// division would not be a convex combination).
|
||||
#[must_use]
|
||||
pub(crate) fn hull_point(cw: CPoint) -> Option<Vec3> {
|
||||
if cw.z == NOZ {
|
||||
return None;
|
||||
}
|
||||
let p = if cw.w == NOW {
|
||||
Vec3::new(cw.x, cw.y, cw.z)
|
||||
} else if cw.w.is_finite() && cw.w > 0.0 {
|
||||
Vec3::new(cw.x / cw.w, cw.y / cw.w, cw.z / cw.w)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
(p.x.is_finite() && p.y.is_finite() && p.z.is_finite()).then_some(p)
|
||||
}
|
||||
|
||||
/// The indices `i` of the non-empty knot spans `U[i] < U[i+1]` (`i` in `p..=n`) of a
|
||||
/// degree `p` curve (or one direction of a surface) with last control index `n`; `None`
|
||||
/// unless the knots are `n + p + 2` finite, non-decreasing values.
|
||||
#[must_use]
|
||||
pub(crate) fn nonempty_spans(knots: &[f64], p: usize, n: usize) -> Option<Vec<usize>> {
|
||||
if knots.len() != n.checked_add(p)?.checked_add(2)?
|
||||
|| knots.iter().any(|k| !k.is_finite())
|
||||
|| knots.windows(2).any(|w| matches!(w, [a, b] if a > b))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
(p..=n)
|
||||
.filter(|&i| matches!((knots.get(i), knots.get(i + 1)), (Some(a), Some(b)) if a < b))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The box of a set of points (`None` when empty).
|
||||
#[must_use]
|
||||
pub(crate) fn points_box(points: impl IntoIterator<Item = Vec3>) -> Option<Extent3d> {
|
||||
let mut points = points.into_iter();
|
||||
let mut bx = Extent3d::from_point(points.next()?);
|
||||
for p in points {
|
||||
bx.add_point(p);
|
||||
}
|
||||
Some(bx)
|
||||
}
|
||||
|
||||
/// One box per non-empty knot span of `cur` that meets `ivl`, in knot order: the piece
|
||||
/// of the curve on span `[U[i], U[i+1]]` is a convex combination of control points
|
||||
/// `i-p..=i` (non-negative weights summing to one, also for a rational curve whose
|
||||
/// weights are all positive, whose Euclidean points are then the convex-combination
|
||||
/// vertices), so it lies in the box of those Euclidean points.
|
||||
///
|
||||
/// `None`, meaning no certified boxes, when a weight is not positive and finite, the
|
||||
/// curve is 2D, a knot is not finite or the knots are not sized for the net, or `ivl` is
|
||||
/// not a finite interval inside the natural domain `[U[p], U[n+1]]`.
|
||||
///
|
||||
/// Not upstream.
|
||||
#[must_use]
|
||||
pub fn crv_span_boxes(cur: &Curve, ivl: &Extent1d) -> Option<Vec<Extent3d>> {
|
||||
let (p, n) = (cur.degree(), cur.n());
|
||||
let knots = cur.knots();
|
||||
let spans = nonempty_spans(knots, p, n)?;
|
||||
let (lo, hi) = (ivl.min(), ivl.max());
|
||||
if !(lo.is_finite() && hi.is_finite() && lo <= hi)
|
||||
|| lo < *knots.get(p)?
|
||||
|| hi > *knots.get(n.checked_add(1)?)?
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let points: Vec<Vec3> = cur
|
||||
.cpts()
|
||||
.iter()
|
||||
.map(|&c| hull_point(c))
|
||||
.collect::<Option<_>>()?;
|
||||
let mut boxes = Vec::new();
|
||||
for i in spans {
|
||||
if *knots.get(i)? <= hi && *knots.get(i + 1)? >= lo {
|
||||
boxes.push(points_box(
|
||||
points.get(i.checked_sub(p)?..=i)?.iter().copied(),
|
||||
)?);
|
||||
}
|
||||
}
|
||||
Some(boxes)
|
||||
}
|
||||
|
||||
/// Rounding allowance for boxes of pieces made by `levels` convex-combination steps
|
||||
/// from control points `cpts` (homogeneous, all weights positive): `16 * levels * eps *
|
||||
/// M * (wmax / wmin)`, with `M` the largest Euclidean coordinate magnitude.
|
||||
///
|
||||
/// Derivation: a step computes `(1-a) P + a Q` with `a` in `[0, 1]` itself rounded; its
|
||||
/// rounding error is at most about `5 eps` times the largest homogeneous component
|
||||
/// (`eps` for `a`, times `|Q - P|`, plus three roundings), and a later step cannot
|
||||
/// amplify an earlier error since the combination weights sum to one. So after
|
||||
/// `levels` steps each homogeneous component is off by at most `8 levels eps` times its
|
||||
/// bound, `wmax * M` for `w x` and `wmax` for `w`. Dividing by the piece's weight, at
|
||||
/// least `wmin` (a convex combination of weights), turns the `w x` error and `|x|` times
|
||||
/// the `w` error into at most `16 levels eps M wmax / wmin`; the division's own
|
||||
/// rounding is within that. `None` when a weight is not positive and finite.
|
||||
#[must_use]
|
||||
pub(crate) fn piece_slack(cpts: impl IntoIterator<Item = CPoint>, levels: usize) -> Option<f64> {
|
||||
let (mut scale, mut wmin, mut wmax) = (0.0_f64, f64::INFINITY, 0.0_f64);
|
||||
for c in cpts {
|
||||
let p = hull_point(c)?;
|
||||
scale = scale.max(p.x.abs()).max(p.y.abs()).max(p.z.abs());
|
||||
let w = if c.w == NOW { 1.0 } else { c.w };
|
||||
wmin = wmin.min(w);
|
||||
wmax = wmax.max(w);
|
||||
}
|
||||
let slack = 16.0 * levels as f64 * f64::EPSILON * scale * (wmax / wmin);
|
||||
slack.is_finite().then_some(slack)
|
||||
}
|
||||
|
||||
/// The control-point boxes of `pieces` (`None` when a point has no hull point).
|
||||
fn curve_piece_boxes(pieces: &[Curve]) -> Option<Vec<Extent3d>> {
|
||||
pieces
|
||||
.iter()
|
||||
.map(|c| {
|
||||
points_box(
|
||||
c.cpts()
|
||||
.iter()
|
||||
.map(|&p| hull_point(p))
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether the knot vector is clamped (its first and last `p + 1` knots equal), which
|
||||
/// Bezier extraction assumes.
|
||||
pub(crate) fn is_clamped(knots: &[f64], p: usize) -> bool {
|
||||
let k = p.saturating_add(1);
|
||||
let ends_equal =
|
||||
|s: Option<&[f64]>| s.is_some_and(|s| s.windows(2).all(|w| matches!(w, [a, b] if a == b)));
|
||||
ends_equal(knots.get(..k))
|
||||
&& ends_equal(knots.len().checked_sub(k).and_then(|i| knots.get(i..)))
|
||||
}
|
||||
|
||||
/// A certified cover of a curve over an interval that can be refined span by span.
|
||||
///
|
||||
/// Level 0 is [`crv_span_boxes`]. [`CurveCover::pieces`] splits a span's Bezier segment
|
||||
/// (`create_bez_segments`) at its parameter midpoint (`crv_split`) `depth` times and
|
||||
/// returns the control-point boxes of the `2^depth` pieces, the allowance
|
||||
/// [`CurveCover::slack`] is to be added to distances to them; made once per span and kept. Where a
|
||||
/// refinement cannot be made (unclamped knots, a split or weight failure) the span
|
||||
/// keeps its level 0 box: a coarser, still certified cover, never an error.
|
||||
///
|
||||
/// Not upstream.
|
||||
pub struct CurveCover {
|
||||
curve: Curve,
|
||||
/// Indices into the non-empty spans of the curve, one per level 0 box.
|
||||
spans: Vec<usize>,
|
||||
coarse: Vec<Extent3d>,
|
||||
depth: usize,
|
||||
slack: f64,
|
||||
segments: Option<Vec<Curve>>,
|
||||
refined: Vec<Option<Vec<Extent3d>>>,
|
||||
}
|
||||
|
||||
impl CurveCover {
|
||||
/// The cover of `cur` over `ivl`, to be refined `depth` times; `None` as for
|
||||
/// [`crv_span_boxes`].
|
||||
#[must_use]
|
||||
pub fn new(cur: &Curve, ivl: &Extent1d, depth: usize) -> Option<Self> {
|
||||
let coarse = crv_span_boxes(cur, ivl)?;
|
||||
let p = cur.degree();
|
||||
let knots = cur.knots();
|
||||
let all = nonempty_spans(knots, p, cur.n())?;
|
||||
let (lo, hi) = (ivl.min(), ivl.max());
|
||||
let spans: Vec<usize> = all
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(_, &i)| {
|
||||
matches!((knots.get(i), knots.get(i + 1)), (Some(&a), Some(&b)) if a <= hi && b >= lo)
|
||||
})
|
||||
.map(|(k, _)| k)
|
||||
.collect();
|
||||
// Bezier extraction chains at most `p` insertions per span, each split of a
|
||||
// degree `p` curve `p` more.
|
||||
let levels = p.saturating_mul(all.len().saturating_add(depth));
|
||||
let slack = piece_slack(cur.cpts().iter().copied(), levels)?;
|
||||
let refined = vec![None; coarse.len()];
|
||||
Some(Self {
|
||||
curve: cur.clone(),
|
||||
spans,
|
||||
coarse,
|
||||
depth,
|
||||
slack,
|
||||
segments: None,
|
||||
refined,
|
||||
})
|
||||
}
|
||||
|
||||
/// The level 0 boxes, one per span that meets the interval, in knot order.
|
||||
#[must_use]
|
||||
pub fn coarse(&self) -> &[Extent3d] {
|
||||
&self.coarse
|
||||
}
|
||||
|
||||
/// The rounding allowance to add to a distance test against boxes of
|
||||
/// [`CurveCover::pieces`] (see [`piece_slack`]).
|
||||
#[must_use]
|
||||
pub const fn slack(&self) -> f64 {
|
||||
self.slack
|
||||
}
|
||||
|
||||
/// The boxes of span `i` split `depth` times, the level 0 box alone when it cannot
|
||||
/// be refined; empty for an `i` out of range.
|
||||
pub fn pieces(&mut self, i: usize) -> &[Extent3d] {
|
||||
if self.refined.get(i).is_some_and(Option::is_none) {
|
||||
let made = self.refine(i);
|
||||
let fallback = self.coarse.get(i).copied();
|
||||
if let Some(slot) = self.refined.get_mut(i) {
|
||||
*slot = made.or_else(|| fallback.map(|b| vec![b]));
|
||||
}
|
||||
}
|
||||
self.refined
|
||||
.get(i)
|
||||
.and_then(Option::as_deref)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn refine(&mut self, i: usize) -> Option<Vec<Extent3d>> {
|
||||
if !is_clamped(self.curve.knots(), self.curve.degree()) {
|
||||
return None;
|
||||
}
|
||||
if self.segments.is_none() {
|
||||
self.segments = Some(create_bez_segments(&self.curve).ok()?);
|
||||
}
|
||||
let mut level = vec![self.segments.as_ref()?.get(*self.spans.get(i)?)?.clone()];
|
||||
for _ in 0..self.depth {
|
||||
let mut next = Vec::new();
|
||||
for piece in &level {
|
||||
let knots = piece.knots();
|
||||
let mid = 0.5 * (knots.first()? + knots.last()?);
|
||||
let (a, b) = crv_split(piece, mid).ok()?;
|
||||
next.push(a);
|
||||
next.push(b);
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
curve_piece_boxes(&level)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2463,4 +2713,115 @@ mod tests {
|
||||
assert!(allocate_nurb_curve(3, 3, 1 << 48).is_err());
|
||||
assert!(allocate_nurb_curve(3, 3, 7).is_ok());
|
||||
}
|
||||
|
||||
/// A rational quadratic quarter circle of radius 2 in the xy plane, weights
|
||||
/// `1, mid_weight, 1` (`sqrt(2)/2` makes it exact).
|
||||
fn quarter_circle(mid_weight: f64) -> Curve {
|
||||
let pw = vec![
|
||||
CPoint::weighted(Vec3::new(2.0, 0.0, 0.0), 1.0),
|
||||
CPoint::weighted(Vec3::new(2.0, 2.0, 0.0), mid_weight),
|
||||
CPoint::weighted(Vec3::new(0.0, 2.0, 0.0), 1.0),
|
||||
];
|
||||
Curve::new(pw, 2, vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap()
|
||||
}
|
||||
|
||||
/// Samples of `cur` on `[a, b]`.
|
||||
fn samples(cur: &Curve, a: f64, b: f64) -> Vec<(f64, Vec3)> {
|
||||
(0..=40)
|
||||
.map(|i| {
|
||||
let u = a + (b - a) * f64::from(i) / 40.0;
|
||||
(u, cur.eval(u, Side::Left).unwrap())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_boxes_hold_every_sample_of_their_span() {
|
||||
let c = cubic();
|
||||
let boxes = crv_span_boxes(&c, &Extent1d::from_min_max(0.0, 1.0)).unwrap();
|
||||
let spans = [(0.0, 0.3), (0.3, 0.6), (0.6, 1.0)];
|
||||
assert_eq!(boxes.len(), spans.len());
|
||||
for (bx, &(a, b)) in boxes.iter().zip(&spans) {
|
||||
for (u, p) in samples(&c, a, b) {
|
||||
assert!(bx.contains_point(p, 1e-13), "u={u} p={p:?} box={bx:?}");
|
||||
}
|
||||
}
|
||||
// Rational: the Euclidean control points, not the weighted ones, bound the arc.
|
||||
let arc = quarter_circle(std::f64::consts::FRAC_1_SQRT_2);
|
||||
let boxes = crv_span_boxes(&arc, &Extent1d::from_min_max(0.0, 1.0)).unwrap();
|
||||
assert_eq!(boxes.len(), 1);
|
||||
for (u, p) in samples(&arc, 0.0, 1.0) {
|
||||
assert!(boxes[0].contains_point(p, 1e-13), "u={u} p={p:?}");
|
||||
}
|
||||
assert_eq!(boxes[0].max_at(0), 2.0);
|
||||
assert_eq!(boxes[0].max_at(1), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_boxes_only_cover_the_spans_meeting_the_interval() {
|
||||
let c = cubic();
|
||||
let one = crv_span_boxes(&c, &Extent1d::from_min_max(0.4, 0.5)).unwrap();
|
||||
assert_eq!(one.len(), 1);
|
||||
let two = crv_span_boxes(&c, &Extent1d::from_min_max(0.2, 0.4)).unwrap();
|
||||
assert_eq!(two.len(), 2);
|
||||
// Outside the natural domain, reversed, or not finite: no certified cover.
|
||||
for (a, b) in [(-0.1, 0.5), (0.5, 1.1), (f64::NAN, 0.5)] {
|
||||
let ivl = Extent1d::from_raw(a, b);
|
||||
assert!(crv_span_boxes(&c, &ivl).is_none(), "[{a}, {b}]");
|
||||
}
|
||||
assert!(crv_span_boxes(&c, &Extent1d::from_raw(0.6, 0.4)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_positive_or_non_finite_weight_gives_no_boxes() {
|
||||
let ivl = Extent1d::from_min_max(0.0, 1.0);
|
||||
for w in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
assert!(crv_span_boxes(&quarter_circle(w), &ivl).is_none(), "w={w}");
|
||||
assert!(
|
||||
CurveCover::new(&quarter_circle(w), &ivl, 2).is_none(),
|
||||
"w={w}"
|
||||
);
|
||||
}
|
||||
// A 2D curve has no certified 3D cover.
|
||||
let mut flat = quarter_circle(1.0);
|
||||
flat.to_2d();
|
||||
assert!(crv_span_boxes(&flat, &ivl).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refined_pieces_hold_the_curve_and_are_tighter() {
|
||||
let ivl = Extent1d::from_min_max(0.0, 1.0);
|
||||
for curve in [cubic(), quarter_circle(0.6)] {
|
||||
let mut cover = CurveCover::new(&curve, &ivl, 3).unwrap();
|
||||
let coarse = cover.coarse().to_vec();
|
||||
let slack = cover.slack();
|
||||
// 1e-13 of 5 units of coordinate and a few hundred levels: far below 1e-9.
|
||||
assert!(slack > 0.0 && slack < 1e-9, "slack={slack}");
|
||||
let all = samples(&curve, 0.0, 1.0);
|
||||
for (i, whole) in coarse.iter().enumerate() {
|
||||
let pieces = cover.pieces(i).to_vec();
|
||||
assert_eq!(pieces.len(), 8);
|
||||
assert!(pieces.iter().all(|p| p.is_contained_by(whole, slack)));
|
||||
// Pieces are strictly smaller than the span box somewhere.
|
||||
assert!(pieces.iter().all(|p| (0..3)
|
||||
.any(|a| p.max_at(a) - p.min_at(a) < whole.max_at(a) - whole.min_at(a))));
|
||||
for &(_, p) in &all {
|
||||
if whole.contains_point(p, 0.0) {
|
||||
// Every sample inside the span box lies in some piece or on a
|
||||
// neighbouring span's pieces (spans share end points).
|
||||
let near = pieces.iter().any(|b| b.contains_point(p, slack));
|
||||
let elsewhere = coarse.iter().enumerate().any(|(j, _)| {
|
||||
j != i
|
||||
&& cover
|
||||
.pieces(j)
|
||||
.to_vec()
|
||||
.iter()
|
||||
.any(|b| b.contains_point(p, slack))
|
||||
});
|
||||
assert!(near || elsewhere, "p={p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+272
-1
@@ -15,8 +15,11 @@
|
||||
//! | [`decompose_srf`] | `sm_DecomposeSrf` |
|
||||
//! | [`split_srf`] | `sm_SplitSrf` |
|
||||
//! | [`is_nsrf_degenerate`] | `sm_IsNSrfDegenerate` |
|
||||
//! | [`srf_span_boxes`] | none (not upstream) |
|
||||
//!
|
||||
//! Deviations:
|
||||
//! - [`srf_span_boxes`] is not upstream: the boxes of the control points of every knot
|
||||
//! span, a certified cover of the surface used to prune coincidence queries.
|
||||
//! - Out-params become return values. The optional `double*` outputs of
|
||||
//! `sm_ComputeNetConstants` / `sm_ComputeSurfConstants` (NULL = not wanted) are a
|
||||
//! [`ConstantsRequest`] in and [`NetConstants`] (`Some` for each requested value)
|
||||
@@ -85,7 +88,9 @@ use crate::math::{max2, Point2, Point3, Vec3, EFF_ZERO, EFF_ZERO_SQ, PI};
|
||||
use crate::nurbs::index::oob;
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::nurbs::math::CPoint;
|
||||
use crate::nurbs::nurbs_crv::to_euclid;
|
||||
use crate::nurbs::nurbs_crv::{
|
||||
hull_point, is_clamped, nonempty_spans, piece_slack, points_box, to_euclid,
|
||||
};
|
||||
use crate::nurbs::srf_basic::{Direction, Surface};
|
||||
use crate::nurbs::srf_tool::p1 as srf_tool;
|
||||
|
||||
@@ -1122,6 +1127,167 @@ pub fn is_nsrf_degenerate(sur: &Surface, tol3d: f64) -> Result<bool> {
|
||||
Ok(bx.is_point_sized(tol))
|
||||
}
|
||||
|
||||
/// One box per pair of non-empty knot spans of `sur` (u-major, knot order): the piece on
|
||||
/// `[U[i], U[i+1]] x [V[j], V[j+1]]` is a convex combination of control points
|
||||
/// `(i-p..=i) x (j-q..=j)` (the tensor basis is non-negative and sums to one, also for a
|
||||
/// rational surface whose weights are all positive, whose Euclidean points are then the
|
||||
/// convex-combination vertices), so it lies in the box of those Euclidean points. Every
|
||||
/// span of the whole surface is covered, whatever sub-domain a caller uses.
|
||||
///
|
||||
/// `None`, meaning no certified boxes, when a weight is not positive and finite, a
|
||||
/// coordinate is not finite or a knot vector is not finite, non-decreasing and sized for
|
||||
/// the net.
|
||||
///
|
||||
/// Not upstream.
|
||||
#[must_use]
|
||||
pub fn srf_span_boxes(sur: &Surface) -> Option<Vec<Extent3d>> {
|
||||
let (p, q) = sur.degrees();
|
||||
let u_spans = nonempty_spans(sur.u_knots(), p, sur.n())?;
|
||||
let v_spans = nonempty_spans(sur.v_knots(), q, sur.m())?;
|
||||
let net: Vec<Vec<Vec3>> = sur
|
||||
.rows()
|
||||
.map(|row| row.iter().map(|&c| hull_point(c)).collect::<Option<_>>())
|
||||
.collect::<Option<_>>()?;
|
||||
let mut boxes = Vec::new();
|
||||
for &i in &u_spans {
|
||||
let rows = net.get(i.checked_sub(p)?..=i)?;
|
||||
for &j in &v_spans {
|
||||
let cols = j.checked_sub(q)?..=j;
|
||||
let mut points = Vec::new();
|
||||
for row in rows {
|
||||
points.extend(row.get(cols.clone())?.iter().copied());
|
||||
}
|
||||
boxes.push(points_box(points)?);
|
||||
}
|
||||
}
|
||||
Some(boxes)
|
||||
}
|
||||
|
||||
/// The control-point boxes of `pieces` (`None` when a point has no hull point).
|
||||
fn surface_piece_boxes(pieces: &[Surface]) -> Option<Vec<Extent3d>> {
|
||||
pieces
|
||||
.iter()
|
||||
.map(|s| {
|
||||
points_box(
|
||||
s.rows()
|
||||
.flatten()
|
||||
.map(|&c| hull_point(c))
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A certified cover of a surface that can be refined span by span.
|
||||
///
|
||||
/// Level 0 is [`srf_span_boxes`]. [`SurfaceCover::pieces`] takes a span's Bezier patch
|
||||
/// ([`decompose_srf`]), splits it at the midpoints of its parameter ranges ([`split_srf`],
|
||||
/// in u then in v, so into four) `depth` times and returns the control-point boxes of the
|
||||
/// `4^depth` pieces; [`SurfaceCover::slack`] is the rounding allowance to add to
|
||||
/// distances to them. A span is refined once, when first asked, and kept. Where a
|
||||
/// refinement cannot be made (unclamped knots, a decomposition, split or weight failure)
|
||||
/// the span keeps its level 0 box: a coarser, still certified cover, never an error.
|
||||
///
|
||||
/// Not upstream.
|
||||
pub struct SurfaceCover {
|
||||
surface: Surface,
|
||||
/// Positions of each level 0 box's spans among the non-empty u and v spans.
|
||||
spans: Vec<(usize, usize)>,
|
||||
coarse: Vec<Extent3d>,
|
||||
depth: usize,
|
||||
slack: f64,
|
||||
patches: Option<Vec<Vec<Surface>>>,
|
||||
refined: Vec<Option<Vec<Extent3d>>>,
|
||||
}
|
||||
|
||||
impl SurfaceCover {
|
||||
/// The cover of `sur`, to be refined `depth` times; `None` as for [`srf_span_boxes`].
|
||||
#[must_use]
|
||||
pub fn new(sur: &Surface, depth: usize) -> Option<Self> {
|
||||
let coarse = srf_span_boxes(sur)?;
|
||||
let (p, q) = sur.degrees();
|
||||
let nu = nonempty_spans(sur.u_knots(), p, sur.n())?.len();
|
||||
let nv = nonempty_spans(sur.v_knots(), q, sur.m())?.len();
|
||||
let spans = (0..nu)
|
||||
.flat_map(|iu| (0..nv).map(move |iv| (iu, iv)))
|
||||
.collect();
|
||||
// Bezier extraction chains at most `p` insertions per u span and `q` per v
|
||||
// span; each split then takes `p` (u) and `q` (v) more.
|
||||
let levels = p
|
||||
.saturating_mul(nu)
|
||||
.saturating_add(q.saturating_mul(nv))
|
||||
.saturating_add(depth.saturating_mul(p.saturating_add(q)));
|
||||
let slack = piece_slack(sur.rows().flatten().copied(), levels)?;
|
||||
let refined = vec![None; coarse.len()];
|
||||
Some(Self {
|
||||
surface: sur.clone(),
|
||||
spans,
|
||||
coarse,
|
||||
depth,
|
||||
slack,
|
||||
patches: None,
|
||||
refined,
|
||||
})
|
||||
}
|
||||
|
||||
/// The level 0 boxes, one per pair of non-empty spans, u-major, in knot order.
|
||||
#[must_use]
|
||||
pub fn coarse(&self) -> &[Extent3d] {
|
||||
&self.coarse
|
||||
}
|
||||
|
||||
/// The rounding allowance to add to a distance test against boxes of
|
||||
/// [`SurfaceCover::pieces`] (see [`piece_slack`]).
|
||||
#[must_use]
|
||||
pub const fn slack(&self) -> f64 {
|
||||
self.slack
|
||||
}
|
||||
|
||||
/// The boxes of span `i` split `depth` times, the level 0 box alone when it cannot
|
||||
/// be refined; empty for an `i` out of range.
|
||||
pub fn pieces(&mut self, i: usize) -> &[Extent3d] {
|
||||
if self.refined.get(i).is_some_and(Option::is_none) {
|
||||
let made = self.refine(i);
|
||||
let fallback = self.coarse.get(i).copied();
|
||||
if let Some(slot) = self.refined.get_mut(i) {
|
||||
*slot = made.or_else(|| fallback.map(|b| vec![b]));
|
||||
}
|
||||
}
|
||||
self.refined
|
||||
.get(i)
|
||||
.and_then(Option::as_deref)
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn refine(&mut self, i: usize) -> Option<Vec<Extent3d>> {
|
||||
let (p, q) = self.surface.degrees();
|
||||
if !is_clamped(self.surface.u_knots(), p) || !is_clamped(self.surface.v_knots(), q) {
|
||||
return None;
|
||||
}
|
||||
if self.patches.is_none() {
|
||||
self.patches = Some(decompose_srf(&self.surface).ok()?);
|
||||
}
|
||||
let &(iu, iv) = self.spans.get(i)?;
|
||||
let mut level = vec![self.patches.as_ref()?.get(iu)?.get(iv)?.clone()];
|
||||
for _ in 0..self.depth {
|
||||
let mut next = Vec::new();
|
||||
for patch in &level {
|
||||
let (u, v) = (patch.u_knots(), patch.v_knots());
|
||||
let mid_u = 0.5 * (u.first()? + u.last()?);
|
||||
let mid_v = 0.5 * (v.first()? + v.last()?);
|
||||
let (left, right) = split_srf(patch, mid_u, Direction::U).ok()?;
|
||||
for half in [left, right] {
|
||||
let (a, b) = split_srf(&half, mid_v, Direction::V).ok()?;
|
||||
next.push(a);
|
||||
next.push(b);
|
||||
}
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
surface_piece_boxes(&level)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1378,4 +1544,109 @@ mod tests {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// A rational biquadratic Bezier patch (a bulging sheet) with the given weight on
|
||||
/// every control point but the middle one, which gets `centre`.
|
||||
fn rational_patch(corner: f64, centre: f64) -> Surface {
|
||||
let net = (0..3)
|
||||
.map(|i| {
|
||||
(0..3)
|
||||
.map(|j| {
|
||||
let w = if (i, j) == (1, 1) { centre } else { corner };
|
||||
let z = if (i, j) == (1, 1) { 2.0 } else { 0.0 };
|
||||
CPoint::weighted(Vec3::new(f64::from(i), f64::from(j) * 1.5, z), w)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let k = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
|
||||
Surface::new(net, 2, 2, k.clone(), k).unwrap()
|
||||
}
|
||||
|
||||
fn surface_samples(s: &Surface, u: (f64, f64), v: (f64, f64)) -> Vec<Vec3> {
|
||||
let mut out = Vec::new();
|
||||
for a in 0..=12 {
|
||||
for b in 0..=12 {
|
||||
let uu = u.0 + (u.1 - u.0) * f64::from(a) / 12.0;
|
||||
let vv = v.0 + (v.1 - v.0) * f64::from(b) / 12.0;
|
||||
out.push(s.eval(uu, vv, Side::Left, Side::Left).unwrap());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn span_boxes_hold_every_sample_of_their_span() {
|
||||
let s = bicubic();
|
||||
let boxes = srf_span_boxes(&s).unwrap();
|
||||
let (us, vs) = ([(0.0, 0.4), (0.4, 1.0)], [(0.0, 0.5), (0.5, 1.0)]);
|
||||
assert_eq!(boxes.len(), 4);
|
||||
for (iu, &u) in us.iter().enumerate() {
|
||||
for (iv, &v) in vs.iter().enumerate() {
|
||||
let bx = boxes[iu * 2 + iv];
|
||||
for p in surface_samples(&s, u, v) {
|
||||
assert!(bx.contains_point(p, 1e-13), "span {iu},{iv} p={p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rational: de-homogenised control points bound the patch, and the patch really
|
||||
// bulges to z > 0 so the box is not trivial.
|
||||
let r = rational_patch(1.0, 3.0);
|
||||
let boxes = srf_span_boxes(&r).unwrap();
|
||||
assert_eq!(boxes.len(), 1);
|
||||
for p in surface_samples(&r, (0.0, 1.0), (0.0, 1.0)) {
|
||||
assert!(boxes[0].contains_point(p, 1e-13), "p={p:?}");
|
||||
}
|
||||
assert_eq!(boxes[0].max_at(2), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_positive_or_non_finite_weight_gives_no_boxes() {
|
||||
for w in [0.0, -1.0, f64::NAN, f64::INFINITY] {
|
||||
assert!(srf_span_boxes(&rational_patch(1.0, w)).is_none(), "w={w}");
|
||||
assert!(
|
||||
SurfaceCover::new(&rational_patch(w, 1.0), 2).is_none(),
|
||||
"w={w}"
|
||||
);
|
||||
}
|
||||
// Unsized knots: no cover.
|
||||
let mut s = bicubic();
|
||||
s.u_knots_mut()[2] = f64::NAN;
|
||||
assert!(srf_span_boxes(&s).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refined_pieces_hold_the_surface_and_are_tighter() {
|
||||
for s in [bicubic(), rational_patch(1.0, 3.0)] {
|
||||
let mut cover = SurfaceCover::new(&s, 2).unwrap();
|
||||
let coarse = cover.coarse().to_vec();
|
||||
let slack = cover.slack();
|
||||
assert!(slack > 0.0 && slack < 1e-9, "slack={slack}");
|
||||
let (u0, u1) = (s.u_knots()[0], *s.u_knots().last().unwrap());
|
||||
let (v0, v1) = (s.v_knots()[0], *s.v_knots().last().unwrap());
|
||||
let all = surface_samples(&s, (u0, u1), (v0, v1));
|
||||
let mut pieces = Vec::new();
|
||||
for (i, whole) in coarse.iter().enumerate() {
|
||||
let here = cover.pieces(i).to_vec();
|
||||
assert_eq!(here.len(), 16);
|
||||
assert!(here.iter().all(|p| p.is_contained_by(whole, slack)));
|
||||
pieces.extend(here);
|
||||
}
|
||||
// Every sample of the surface lies in some piece box.
|
||||
for p in &all {
|
||||
assert!(
|
||||
pieces.iter().any(|b| b.contains_point(*p, slack)),
|
||||
"p={p:?}"
|
||||
);
|
||||
}
|
||||
// The pieces are tighter than the hull: a point of the coarse box that the
|
||||
// surface never gets near is in no piece.
|
||||
let far: Vec<_> = coarse
|
||||
.iter()
|
||||
.flat_map(|c| [c.min, c.max])
|
||||
.filter(|corner| !pieces.iter().any(|b| b.contains_point(*corner, 1e-3)))
|
||||
.collect();
|
||||
assert!(!far.is_empty(), "refining tightened nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
//! A certified bound of how far apart two B-spline surfaces are at the same parameters.
|
||||
//!
|
||||
//! Upstream: none (not upstream; solidcore-native).
|
||||
//!
|
||||
//! | Rust | Upstream |
|
||||
//! |------------------------|----------|
|
||||
//! | [`certified_distance`] | none |
|
||||
//!
|
||||
//! The surface form of [`crate::nurbs::crv_deviation::certified_distance`]. Both surfaces are
|
||||
//! moved into their own frame (minus the first's corner control point), cut into Bézier
|
||||
//! patches over the union of their breakpoints in u and in v ([`insert_knots`],
|
||||
//! [`decompose_to_bez`]) and each patch raised to the higher degrees ([`elevate_degree`], on a
|
||||
//! patch with no interior knot only the Bézier elevation). On a common patch the difference of
|
||||
//! two patches of one degree is the patch of the differences of their control points, which
|
||||
//! lies in their convex hull (Farouki and Rajan 1987; Piegl and Tiller, *The NURBS Book*, 2nd
|
||||
//! ed., §5.6): at no parameter of the patch are the surfaces farther apart than the largest
|
||||
//! difference.
|
||||
//!
|
||||
//! Rounding (docs/definitions-and-certificates.md §4.4), as for curves: each knot insertion
|
||||
//! forms each new control point as one convex combination (`γ_12 M` per coordinate, `M` the
|
||||
//! nets' largest coordinate in their frame), a point carrying at most the insertions of both
|
||||
//! directions, `I` in all: `γ_(12 (I + 1))` of twice `M`; the elevations `γ_(8 (q_u + q_v))`
|
||||
//! of `M`; the move into the frame `γ_2` of `M`; each difference's length a scaled norm
|
||||
//! rounded up by `γ_8`; a coordinate error `e` moves a point by at most `2 e`.
|
||||
//!
|
||||
//! Deviations: none (no upstream counterpart). Non-rational 3D surfaces only, of degrees 1 to
|
||||
//! [`MAX_DEGREE`], their knot vectors clamped on one domain.
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::nurbs::crv_deviation::norm_up;
|
||||
use crate::nurbs::knots::MAX_DEGREE;
|
||||
use crate::nurbs::math::{gamma, CPoint, NOW, NOZ};
|
||||
use crate::nurbs::srf_basic::{Direction, Surface};
|
||||
use crate::nurbs::srf_tool::p1::{decompose_to_bez, insert_knots};
|
||||
use crate::nurbs::srf_tool::p2::elevate_degree;
|
||||
|
||||
const OP: &str = "nurbs::srf_deviation::certified_distance";
|
||||
|
||||
/// A bound `d` with `|a(u, v) - b(u, v)| <= d` at every parameter of the surfaces' common
|
||||
/// domain, every rounding of computing it included: the largest distance between the control
|
||||
/// points of their Bézier patches raised to one pair of degrees over common patches (module
|
||||
/// docs). A surface and itself, with knots inserted or degrees raised, are within rounding.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a rational or 2D surface, a number not finite, a degree outside
|
||||
/// `1..=MAX_DEGREE` or knots that do not fit the net, knots not clamped, or two surfaces on
|
||||
/// different domains; a query failure when their patches do not match or the bound is not
|
||||
/// finite; those of the refinement, the extraction and the elevation.
|
||||
pub fn certified_distance(a: &Surface, b: &Surface) -> Result<f64> {
|
||||
check(a)?;
|
||||
check(b)?;
|
||||
let ends = |k: &[f64]| (k.first().copied(), k.last().copied());
|
||||
if ends(a.u_knots()) != ends(b.u_knots()) || ends(a.v_knots()) != ends(b.v_knots()) {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"the surfaces are not on one domain",
|
||||
));
|
||||
}
|
||||
// In the surfaces' own frame, so the rounding counted follows their size.
|
||||
let c0 = a.cpt(0, 0)?;
|
||||
let local = |s: &Surface| -> Result<Surface> {
|
||||
let net = s
|
||||
.rows()
|
||||
.map(|row| {
|
||||
row.iter()
|
||||
.map(|p| CPoint::new(p.x - c0.x, p.y - c0.y, p.z - c0.z, NOW))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
Surface::new(
|
||||
net,
|
||||
s.p(),
|
||||
s.q(),
|
||||
s.u_knots().to_vec(),
|
||||
s.v_knots().to_vec(),
|
||||
)
|
||||
};
|
||||
let (a_local, b_local) = (local(a)?, local(b)?);
|
||||
let (a, b) = (&a_local, &b_local);
|
||||
let degrees = (a.p().max(b.p()), a.q().max(b.q()));
|
||||
let size = largest_coordinate(a).max(largest_coordinate(b));
|
||||
let moved = gamma(2) * size;
|
||||
let (pa, ea) = patches(a, b, degrees, size)?;
|
||||
let (pb, eb) = patches(b, a, degrees, size)?;
|
||||
let mismatch = || SolidError::query_failed(OP, "the surfaces' patches do not match");
|
||||
if pa.len() != pb.len() {
|
||||
return Err(mismatch());
|
||||
}
|
||||
let mut largest = 0.0_f64;
|
||||
for (ra, rb) in pa.iter().zip(&pb) {
|
||||
if ra.len() != rb.len() {
|
||||
return Err(mismatch());
|
||||
}
|
||||
for (x, y) in ra.iter().zip(rb) {
|
||||
if ends(x.u_knots()) != ends(y.u_knots())
|
||||
|| ends(x.v_knots()) != ends(y.v_knots())
|
||||
|| x.degrees() != y.degrees()
|
||||
{
|
||||
return Err(mismatch());
|
||||
}
|
||||
for (row_x, row_y) in x.rows().zip(y.rows()) {
|
||||
if row_x.len() != row_y.len() {
|
||||
return Err(mismatch());
|
||||
}
|
||||
for (p, r) in row_x.iter().zip(row_y) {
|
||||
let d = norm_up(p.x - r.x, p.y - r.y, p.z - r.z)
|
||||
.ok_or_else(|| SolidError::query_failed(OP, "the bound is not finite"))?;
|
||||
largest = largest.max(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let bound = (largest + 2.0 * (ea + eb + 2.0 * moved)) * (1.0 + gamma(2));
|
||||
if !bound.is_finite() {
|
||||
return Err(SolidError::query_failed(OP, "the bound is not finite"));
|
||||
}
|
||||
Ok(bound)
|
||||
}
|
||||
|
||||
/// Refuses a surface the bound does not take (see [`certified_distance`]'s errors).
|
||||
fn check(s: &Surface) -> Result<()> {
|
||||
let (p, q) = s.degrees();
|
||||
let rows = s.n().saturating_add(1);
|
||||
let cols = s.m().saturating_add(1);
|
||||
if !((1..=MAX_DEGREE).contains(&p)
|
||||
&& (1..=MAX_DEGREE).contains(&q)
|
||||
&& s.u_knots().len() == rows + p + 1
|
||||
&& s.v_knots().len() == cols + q + 1)
|
||||
{
|
||||
return Err(SolidError::invalid_input(
|
||||
OP,
|
||||
"a degree of 0 or past the most, or knots that do not fit the net",
|
||||
));
|
||||
}
|
||||
let points_ok = s.rows().all(|row| {
|
||||
row.iter()
|
||||
.all(|c| c.x.is_finite() && c.y.is_finite() && c.z.is_finite())
|
||||
});
|
||||
let knots_ok = s.u_knots().iter().chain(s.v_knots()).all(|k| k.is_finite());
|
||||
if !(points_ok && knots_ok) {
|
||||
return Err(SolidError::invalid_input(OP, "numbers not finite"));
|
||||
}
|
||||
if s.rows().flatten().any(|c| c.w != NOW) {
|
||||
return Err(SolidError::invalid_input(OP, "a rational surface"));
|
||||
}
|
||||
if s.rows().flatten().any(|c| c.z == NOZ) {
|
||||
return Err(SolidError::invalid_input(OP, "a 2D surface"));
|
||||
}
|
||||
if !clamped(s.u_knots(), p) || !clamped(s.v_knots(), q) {
|
||||
return Err(SolidError::invalid_input(OP, "knots not clamped"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the first and last `degree + 1` of `knots` are each one value.
|
||||
fn clamped(knots: &[f64], degree: usize) -> bool {
|
||||
let flat =
|
||||
|run: Option<&[f64]>| run.is_some_and(|r| r.windows(2).all(|w| w.first() == w.get(1)));
|
||||
let last = knots.len().checked_sub(degree + 1);
|
||||
flat(knots.get(..=degree)) && flat(last.and_then(|s| knots.get(s..)))
|
||||
}
|
||||
|
||||
/// The largest absolute coordinate of `s`'s control points.
|
||||
fn largest_coordinate(s: &Surface) -> f64 {
|
||||
s.rows().flatten().fold(0.0_f64, |m, p| {
|
||||
m.max(p.x.abs()).max(p.y.abs()).max(p.z.abs())
|
||||
})
|
||||
}
|
||||
|
||||
/// The interior knots of `other` that `knots` lacks, once each, in order.
|
||||
fn missing(knots: &[f64], other: &[f64]) -> Vec<f64> {
|
||||
let (Some(&lo), Some(&hi)) = (knots.first(), knots.last()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out: Vec<f64> = Vec::new();
|
||||
for &u in other {
|
||||
if u > lo && u < hi && !knots.contains(&u) && out.last() != Some(&u) {
|
||||
out.push(u);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The Bézier patches of `s` over the union of its breakpoints and `other`'s, each raised to
|
||||
/// `degrees`, and the error each of their coordinates may carry (module docs), `size` the
|
||||
/// largest coordinate of both nets.
|
||||
fn patches(
|
||||
s: &Surface,
|
||||
other: &Surface,
|
||||
(qu, qv): (usize, usize),
|
||||
size: f64,
|
||||
) -> Result<(Vec<Vec<Surface>>, f64)> {
|
||||
let mut refined = s.clone();
|
||||
let in_u = missing(s.u_knots(), other.u_knots());
|
||||
if !in_u.is_empty() {
|
||||
refined = insert_knots(&refined, &in_u, Direction::U)?;
|
||||
}
|
||||
let in_v = missing(s.v_knots(), other.v_knots());
|
||||
if !in_v.is_empty() {
|
||||
refined = insert_knots(&refined, &in_v, Direction::V)?;
|
||||
}
|
||||
let (p, q) = s.degrees();
|
||||
let pieces = decompose_to_bez(&refined)?;
|
||||
// knots inserted: those of the patches' joined knot vectors, 2 (p + 1) + (k - 1) p for k
|
||||
// patches along a direction, past s's own
|
||||
let joined = |count: usize, degree: usize| {
|
||||
count
|
||||
.saturating_sub(1)
|
||||
.saturating_mul(degree)
|
||||
.saturating_add(2 * (degree + 1))
|
||||
};
|
||||
let along_v = pieces.first().map_or(0, Vec::len);
|
||||
let inserted = joined(pieces.len(), p)
|
||||
.saturating_sub(s.u_knots().len())
|
||||
.saturating_add(joined(along_v, q).saturating_sub(s.v_knots().len()));
|
||||
let mut err = gamma(inserted.saturating_add(1).saturating_mul(12)) * 2.0 * size;
|
||||
let (ru, rv) = (qu.saturating_sub(p), qv.saturating_sub(q));
|
||||
if ru == 0 && rv == 0 {
|
||||
return Ok((pieces, err));
|
||||
}
|
||||
err += gamma(qu.saturating_add(qv).saturating_mul(8)) * (size + err);
|
||||
let mut raised = Vec::new();
|
||||
for row in &pieces {
|
||||
let mut out = Vec::new();
|
||||
for patch in row {
|
||||
let mut x = patch.clone();
|
||||
if ru > 0 {
|
||||
x = elevate_degree(&x, ru, Direction::U)?;
|
||||
}
|
||||
if rv > 0 {
|
||||
x = elevate_degree(&x, rv, Direction::V)?;
|
||||
}
|
||||
out.push(x);
|
||||
}
|
||||
raised.push(out);
|
||||
}
|
||||
Ok((raised, err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::math::Vec3;
|
||||
use crate::nurbs::geom::Rectangle;
|
||||
use crate::nurbs::knots::{clamped_uniform, Side};
|
||||
|
||||
/// A bicubic on a 5 x 4 net over [0, 1]², a gentle hill.
|
||||
fn hill() -> Surface {
|
||||
let net = (0..5)
|
||||
.map(|i| {
|
||||
(0..4)
|
||||
.map(|j| {
|
||||
let (x, y) = (f64::from(i) * 2.0, f64::from(j) * 3.0);
|
||||
let z = f64::from((i * j) % 3) - 0.5 * f64::from(j);
|
||||
CPoint::from_point(Vec3::new(x, y, z))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
Surface::new(
|
||||
net,
|
||||
3,
|
||||
3,
|
||||
clamped_uniform(5, 3).unwrap(),
|
||||
clamped_uniform(4, 3).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_surface_is_no_distance_from_itself_or_its_refinements() {
|
||||
let s = hill();
|
||||
let d = certified_distance(&s, &s).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
let more = insert_knots(&s, &[0.3, 0.7], Direction::U).unwrap();
|
||||
let higher = elevate_degree(&s, 2, Direction::V).unwrap();
|
||||
for other in [&more, &higher] {
|
||||
let d = certified_distance(&s, other).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
let d = certified_distance(other, &s).unwrap();
|
||||
assert!(d < 1e-12, "{d}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_moved_control_point_is_bounded_by_its_move_and_holds_every_sampled_gap() {
|
||||
let s = hill();
|
||||
let mut m = s.clone();
|
||||
m.cpt_mut(2, 1).unwrap().z += 1.0;
|
||||
let d = certified_distance(&s, &m).unwrap();
|
||||
assert!(d <= 1.0 + 1e-12, "{d}");
|
||||
for i in 0..=40 {
|
||||
for j in 0..=40 {
|
||||
let (u, v) = (f64::from(i) / 40.0, f64::from(j) / 40.0);
|
||||
let p = s.eval(u, v, Side::Left, Side::Left).unwrap();
|
||||
let q = m.eval(u, v, Side::Left, Side::Left).unwrap();
|
||||
let gap2 = (p.x - q.x).powi(2) + (p.y - q.y).powi(2) + (p.z - q.z).powi(2);
|
||||
assert!(gap2 <= d * d, "at ({u}, {v}): {gap2} past {d}²");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_bound_follows_the_surfaces_size_not_their_place() {
|
||||
let mut far = hill();
|
||||
for i in 0..5 {
|
||||
for j in 0..4 {
|
||||
far.cpt_mut(i, j).unwrap().x += 1e9;
|
||||
}
|
||||
}
|
||||
let d = certified_distance(&far, &far).unwrap();
|
||||
assert!(d < 1e-9, "{d}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surfaces_the_bound_does_not_take_are_refused() {
|
||||
let s = hill();
|
||||
let mut other = s.clone();
|
||||
other
|
||||
.reparam_to_interval(
|
||||
Rectangle {
|
||||
ul: 0.0,
|
||||
ur: 2.0,
|
||||
vb: 0.0,
|
||||
vt: 1.0,
|
||||
},
|
||||
Direction::U,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
certified_distance(&s, &other),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
let mut rational = s.clone();
|
||||
rational.non_rat_to_rat();
|
||||
assert!(matches!(
|
||||
certified_distance(&s, &rational),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
let mut nan = s.clone();
|
||||
nan.cpt_mut(1, 1).unwrap().y = f64::NAN;
|
||||
assert!(matches!(
|
||||
certified_distance(&s, &nan),
|
||||
Err(SolidError::InvalidInput { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
+395
-499
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user