mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
One kernel flag: solidcore, the default, or smlib; routed is gone
The process runs on one kernel, so `Choice` and the routing table (`Op`, `Overrides`,
SOLIDCRAFT_KERNEL_ROUTES) go, and every call asks `route::backend()`. SOLIDCRAFT_KERNEL takes smlib or
solidcore as before.
- The preference offers solidcore and SMLib. A saved `routed` loads as solidcore without an error;
asked for now, by ui.prefs or diagnostics.kernel, it is refused. Another unknown saved value stays
as saved and runs on the default, as it did.
- diagnostics.kernel answers {choice, environment}. `smlib` on a build without the C++ kernel, wasm
included, is refused as not available, never answered by the other kernel; a saved `smlib` there
says so on the status line and the preference page.
- The conversion between kernels stays: a body computed before the choice changed still needs it, and
so do the parity tests.
The notes' translations in 6 languages and the new message's are machine-written, not reviewed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
@@ -668,9 +668,10 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft
|
||||
return out;
|
||||
}
|
||||
};
|
||||
// The C++ kernel is needed only if a family is routed to it.
|
||||
let route = solidcraft_engine::kernel::kernel_route::Op::ALL;
|
||||
if !solidcraft_engine::kernel::smlib_built() && route.iter().any(|op| op.backend() == solidcraft_engine::kernel::kernel_route::Backend::Smlib) {
|
||||
// The C++ kernel is needed only if it is the one asked for.
|
||||
if !solidcraft_engine::kernel::smlib_built()
|
||||
&& solidcraft_engine::kernel::kernel_route::backend() == solidcraft_engine::kernel::kernel_route::Backend::Smlib
|
||||
{
|
||||
out["status"] = json!("unsupported");
|
||||
out["error"] = json!("this build has no SMLib: build solidcraft-cli with --features smlib");
|
||||
return out;
|
||||
|
||||
@@ -3,49 +3,49 @@
|
||||
"cmd.diagnostics_kernel": "Geometry Kernel",
|
||||
"prefs.geometry_kernel": "Geometry kernel",
|
||||
"prefs.geometry_kernel_environment": "The SOLIDCRAFT_KERNEL environment variable is set; it wins over this setting.",
|
||||
"prefs.geometry_kernel_note": "Routed uses solidcore, the default, for every operation. Choose SMLib for the C++ kernel. A solidcore failure is reported, never retried on SMLib.",
|
||||
"prefs.geometry_kernel_routed": "Routed"
|
||||
"prefs.geometry_kernel_note": "solidcore, the default, answers every operation. Choose SMLib for the C++ kernel. A solidcore failure is reported, never retried on SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib is not available in this build. Operations run on solidcore."
|
||||
},
|
||||
"ar": {
|
||||
"cmd.diagnostics_kernel": "نواة الهندسة",
|
||||
"prefs.geometry_kernel": "نواة الهندسة",
|
||||
"prefs.geometry_kernel_environment": "متغير البيئة SOLIDCRAFT_KERNEL مضبوط، وله الأولوية على هذا الإعداد.",
|
||||
"prefs.geometry_kernel_note": "يستخدم الوضع الموجَّه، وهو الافتراضي، solidcore لكل العمليات. اختر SMLib لاستخدام نواة C++. ويُبلَّغ عن إخفاق solidcore ولا تُعاد المحاولة أبدًا على SMLib.",
|
||||
"prefs.geometry_kernel_routed": "موجَّه"
|
||||
"prefs.geometry_kernel_note": "يجيب solidcore، وهو الافتراضي، عن كل العمليات. اختر SMLib لاستخدام نواة C++. ويُبلَّغ عن إخفاق solidcore ولا تُعاد المحاولة أبدًا على SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib غير متاح في هذا الإصدار. تعمل العمليات على solidcore."
|
||||
},
|
||||
"de": {
|
||||
"cmd.diagnostics_kernel": "Geometriekernel",
|
||||
"prefs.geometry_kernel": "Geometriekernel",
|
||||
"prefs.geometry_kernel_environment": "Die Umgebungsvariable SOLIDCRAFT_KERNEL ist gesetzt; sie hat Vorrang vor dieser Einstellung.",
|
||||
"prefs.geometry_kernel_note": "Geroutet, die Standardeinstellung, nutzt solidcore für alle Vorgänge. Wählen Sie SMLib für den C++-Kernel. Ein Fehler in solidcore wird gemeldet, nie mit SMLib wiederholt.",
|
||||
"prefs.geometry_kernel_routed": "Geroutet"
|
||||
"prefs.geometry_kernel_note": "solidcore, die Standardeinstellung, führt alle Vorgänge aus. Wählen Sie SMLib für den C++-Kernel. Ein Fehler in solidcore wird gemeldet, nie mit SMLib wiederholt.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib ist in diesem Build nicht verfügbar. Vorgänge laufen auf solidcore."
|
||||
},
|
||||
"fr": {
|
||||
"cmd.diagnostics_kernel": "Noyau géométrique",
|
||||
"prefs.geometry_kernel": "Noyau géométrique",
|
||||
"prefs.geometry_kernel_environment": "La variable d’environnement SOLIDCRAFT_KERNEL est définie ; elle l’emporte sur ce réglage.",
|
||||
"prefs.geometry_kernel_note": "Routé, le réglage par défaut, utilise solidcore pour toutes les opérations. Choisissez SMLib pour le noyau C++. Un échec de solidcore est signalé, jamais réessayé avec SMLib.",
|
||||
"prefs.geometry_kernel_routed": "Routé"
|
||||
"prefs.geometry_kernel_note": "solidcore, le réglage par défaut, traite toutes les opérations. Choisissez SMLib pour le noyau C++. Un échec de solidcore est signalé, jamais réessayé avec SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib n’est pas disponible dans cette version. Les opérations s’exécutent sur solidcore."
|
||||
},
|
||||
"he": {
|
||||
"cmd.diagnostics_kernel": "ליבת גאומטריה",
|
||||
"prefs.geometry_kernel": "ליבת גאומטריה",
|
||||
"prefs.geometry_kernel_environment": "משתנה הסביבה SOLIDCRAFT_KERNEL מוגדר, והוא גובר על הגדרה זו.",
|
||||
"prefs.geometry_kernel_note": "מנותב, ברירת המחדל, משתמש ב-solidcore לכל הפעולות. בחרו ב-SMLib עבור ליבת ה-C++. כשל של solidcore מדווח, ולעולם אינו מנוסה שוב ב-SMLib.",
|
||||
"prefs.geometry_kernel_routed": "מנותב"
|
||||
"prefs.geometry_kernel_note": "solidcore, ברירת המחדל, מבצע את כל הפעולות. בחרו ב-SMLib עבור ליבת ה-C++. כשל של solidcore מדווח, ולעולם אינו מנוסה שוב ב-SMLib.",
|
||||
"prefs.geometry_kernel_unavailable": "SMLib אינו זמין בגרסה זו. הפעולות מתבצעות ב-solidcore."
|
||||
},
|
||||
"ja": {
|
||||
"cmd.diagnostics_kernel": "ジオメトリ カーネル",
|
||||
"prefs.geometry_kernel": "ジオメトリ カーネル",
|
||||
"prefs.geometry_kernel_environment": "環境変数 SOLIDCRAFT_KERNEL が設定されているため、この設定より優先されます。",
|
||||
"prefs.geometry_kernel_note": "既定のルーティングでは、すべての処理に solidcore を使います。C++ カーネルを使うには SMLib を選択してください。solidcore の失敗は報告され、SMLib で再試行されることはありません。",
|
||||
"prefs.geometry_kernel_routed": "ルーティング"
|
||||
"prefs.geometry_kernel_note": "既定の solidcore がすべての処理を行います。C++ カーネルを使うには SMLib を選択してください。solidcore の失敗は報告され、SMLib で再試行されることはありません。",
|
||||
"prefs.geometry_kernel_unavailable": "このビルドでは SMLib を利用できません。処理は solidcore で実行されます。"
|
||||
},
|
||||
"zh": {
|
||||
"cmd.diagnostics_kernel": "几何内核",
|
||||
"prefs.geometry_kernel": "几何内核",
|
||||
"prefs.geometry_kernel_environment": "已设置环境变量 SOLIDCRAFT_KERNEL,它优先于此设置。",
|
||||
"prefs.geometry_kernel_note": "默认的路由模式对所有操作使用 solidcore。如需使用 C++ 内核,请选择 SMLib。solidcore 失败时会报告,绝不会改用 SMLib 重试。",
|
||||
"prefs.geometry_kernel_routed": "路由"
|
||||
"prefs.geometry_kernel_note": "默认的 solidcore 处理所有操作。如需使用 C++ 内核,请选择 SMLib。solidcore 失败时会报告,绝不会改用 SMLib 重试。",
|
||||
"prefs.geometry_kernel_unavailable": "此版本不提供 SMLib。操作在 solidcore 上运行。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,10 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
--profile and the app's preference; off, it costs nothing measurable. Read-only",
|
||||
),
|
||||
CommandSpec::new("diagnostics.kernel", "Geometry Kernel", kernel).icon("bug_report").noundo().params(
|
||||
"choice?: routed | smlib | solidcore (sets which kernel answers: routed, the default = each operation on the kernel \
|
||||
the routing table gives it, solidcore for every one; smlib = the C++ SMLib kernel for all; solidcore = the Rust port \
|
||||
for all; bodies already computed keep their results). SOLIDCRAFT_KERNEL, when set, wins over it, and SOLIDCRAFT_KERNEL_ROUTES (op=backend,...) \
|
||||
moves single operations on top of both. Returns {choice, environment, overrides: {op: backend}, routes: [{op, \
|
||||
route, backend}]}: route is the table's kernel for the operation, backend the one it runs on now",
|
||||
"choice?: solidcore | smlib (sets which kernel answers every operation: solidcore, the default = the Rust port; \
|
||||
smlib = the C++ SMLib kernel, refused as not available on a build without it, such as the web app; bodies already \
|
||||
computed keep their results). SOLIDCRAFT_KERNEL, when set, wins over it. Returns {choice, environment}: choice is \
|
||||
the kernel in force, environment whether SOLIDCRAFT_KERNEL chose it",
|
||||
),
|
||||
CommandSpec::new("diagnostics.validation", "Validation", crate::checked!("diagnostics.validation", validation))
|
||||
.icon("bug_report")
|
||||
@@ -66,15 +65,22 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
|
||||
fn kernel(_: &mut Session, p: &Value) -> Result<Value> {
|
||||
use solidcraft_kernel::kernel_route as route;
|
||||
// A choice of the wrong type is refused like an unknown one, not read as no choice.
|
||||
if let Some(v) = p.get("choice").filter(|v| !v.is_null()) {
|
||||
let c = v.as_str().and_then(route::Choice::from_id).ok_or_else(|| bad("diagnostics.kernel", "`choice` is routed, smlib or solidcore"))?;
|
||||
route::set_choice(c);
|
||||
route::set_choice(kernel_asked(v, solidcraft_kernel::smlib_built())?);
|
||||
}
|
||||
let routes: Vec<Value> =
|
||||
route::Op::ALL.iter().map(|op| json!({ "op": op.id(), "route": op.route().id(), "backend": op.backend().id() })).collect();
|
||||
let overrides: serde_json::Map<String, Value> = route::overrides().iter().map(|(op, b)| (op.id().to_string(), json!(b.id()))).collect();
|
||||
Ok(json!({ "choice": route::choice().id(), "environment": route::overridden(), "overrides": overrides, "routes": routes }))
|
||||
Ok(json!({ "choice": route::choice().id(), "environment": route::overridden() }))
|
||||
}
|
||||
|
||||
/// The kernel `choice` asks for, on a build that has (`built`) the C++ kernel or doesn't. A choice of
|
||||
/// the wrong type is refused like an unknown one, not read as no choice; `smlib` on a build without
|
||||
/// it is refused too, never answered by the other kernel.
|
||||
pub(crate) fn kernel_asked(choice: &Value, built: bool) -> Result<solidcraft_kernel::kernel_route::Backend> {
|
||||
use solidcraft_kernel::kernel_route::Backend;
|
||||
let asked = choice.as_str().and_then(Backend::from_id).ok_or_else(|| bad("diagnostics.kernel", "`choice` is solidcore or smlib"))?;
|
||||
if asked == Backend::Smlib && !built {
|
||||
return Err(EngineError::Disabled("diagnostics.kernel".into(), "this build has no C++ SMLib kernel".into()));
|
||||
}
|
||||
Ok(asked)
|
||||
}
|
||||
|
||||
fn dir(cmd: &str) -> Result<std::path::PathBuf> {
|
||||
|
||||
@@ -30,7 +30,7 @@ mod component_clone;
|
||||
mod component_clone_assembly;
|
||||
mod construct;
|
||||
mod delete;
|
||||
mod diagnostics;
|
||||
pub(crate) mod diagnostics;
|
||||
mod edit;
|
||||
mod face;
|
||||
mod features;
|
||||
|
||||
@@ -64,8 +64,8 @@ fn diagnostics_profile_reports_commands_features_and_the_kernel() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Tests that set the process-wide kernel choice run one at a time, and put `routed` back however
|
||||
/// they end (a failed assertion included).
|
||||
/// Tests that set the process-wide kernel choice run one at a time, and put `solidcore` back
|
||||
/// however they end (a failed assertion included).
|
||||
struct KernelChoice(#[allow(dead_code)] std::sync::MutexGuard<'static, ()>);
|
||||
|
||||
static KERNEL_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
@@ -78,57 +78,44 @@ impl KernelChoice {
|
||||
|
||||
impl Drop for KernelChoice {
|
||||
fn drop(&mut self) {
|
||||
solidcraft_kernel::kernel_route::set_choice(solidcraft_kernel::kernel_route::Choice::Routed);
|
||||
solidcraft_kernel::kernel_route::set_choice(solidcraft_kernel::kernel_route::Backend::Solidcore);
|
||||
}
|
||||
}
|
||||
|
||||
/// diagnostics.kernel lists every operation family with the table's kernel and the one it runs
|
||||
/// on now, and its `choice` moves them (unless SOLIDCRAFT_KERNEL overrides it).
|
||||
/// diagnostics.kernel says the kernel in force and whether the environment chose it, and its
|
||||
/// `choice` sets it (unless SOLIDCRAFT_KERNEL overrides it).
|
||||
///
|
||||
/// The choice is process-wide and the other tests in this binary build bodies meanwhile, so only
|
||||
/// choices that route every family as the table does are set through the command; what the
|
||||
/// others would route to is read from `Op::backend_under`, which the command's own answer follows.
|
||||
/// `solidcore` is set through the command; what `smlib` asks for is read from `kernel_asked`.
|
||||
#[test]
|
||||
fn diagnostics_kernel_reports_and_sets_the_choice() {
|
||||
use solidcraft_kernel::kernel_route as route;
|
||||
let _choice = KernelChoice::take();
|
||||
let mut s = Session::default();
|
||||
let r = s.execute("diagnostics.kernel", &json!({})).unwrap();
|
||||
let routes = r["routes"].as_array().unwrap();
|
||||
assert_eq!(routes.len(), route::Op::ALL.len(), "{r}");
|
||||
for (row, op) in routes.iter().zip(route::Op::ALL) {
|
||||
assert_eq!(row["op"], json!(format!("{op:?}").to_lowercase()), "{r}");
|
||||
assert_eq!(row["route"], json!(op.route().id()), "{r}");
|
||||
assert!(matches!(row["backend"].as_str(), Some("smlib" | "solidcore")), "{r}");
|
||||
}
|
||||
let environment = route::overridden();
|
||||
assert_eq!(r["environment"], json!(environment), "{r}");
|
||||
// SOLIDCRAFT_KERNEL_ROUTES, as the families it moves.
|
||||
let moved: serde_json::Map<String, serde_json::Value> = route::overrides().iter().map(|(op, b)| (op.id().to_string(), json!(b.id()))).collect();
|
||||
assert_eq!(r["overrides"], serde_json::Value::Object(moved), "{r}");
|
||||
for choice in ["solidcore", "smlib", "routed"] {
|
||||
let c = route::Choice::from_id(choice).unwrap();
|
||||
for op in route::Op::ALL {
|
||||
let want = match choice {
|
||||
"routed" => op.route().id(),
|
||||
other => other,
|
||||
};
|
||||
assert_eq!(op.backend_with(c, &route::Overrides::NONE).id(), want, "{choice}: {op:?}");
|
||||
}
|
||||
if !route::Op::ALL.iter().all(|op| op.backend_under(c) == op.route()) {
|
||||
continue; // it would move other tests' bodies to another kernel
|
||||
}
|
||||
let r = s.execute("diagnostics.kernel", &json!({ "choice": choice })).unwrap();
|
||||
if environment {
|
||||
continue; // SOLIDCRAFT_KERNEL wins: the choice and the backends stay its own
|
||||
}
|
||||
assert_eq!(r["choice"], json!(choice), "{r}");
|
||||
for (row, op) in r["routes"].as_array().unwrap().iter().zip(route::Op::ALL) {
|
||||
assert_eq!(row["backend"], json!(op.backend_under(c).id()), "{choice}: {r}");
|
||||
}
|
||||
assert_eq!(r, json!({ "choice": route::choice().id(), "environment": environment }));
|
||||
let r = s.execute("diagnostics.kernel", &json!({ "choice": "solidcore" })).unwrap();
|
||||
assert!(environment || r["choice"] == json!("solidcore"), "{r}");
|
||||
assert_eq!(crate::cmd::diagnostics::kernel_asked(&json!("solidcore"), false).unwrap(), route::Backend::Solidcore);
|
||||
assert_eq!(crate::cmd::diagnostics::kernel_asked(&json!("solidcore"), true).unwrap(), route::Backend::Solidcore);
|
||||
assert_eq!(crate::cmd::diagnostics::kernel_asked(&json!("smlib"), true).unwrap(), route::Backend::Smlib);
|
||||
}
|
||||
|
||||
/// `smlib` on a build without the C++ kernel is refused as not available, whichever kernel is in
|
||||
/// force, and leaves the choice alone: never an answer from the other kernel. Built with it, the
|
||||
/// command would move the other tests' bodies, so `kernel_asked` alone is asked.
|
||||
#[test]
|
||||
fn diagnostics_kernel_refuses_smlib_on_a_build_without_it() {
|
||||
let _choice = KernelChoice::take();
|
||||
let refused = crate::cmd::diagnostics::kernel_asked(&json!("smlib"), false).unwrap_err();
|
||||
assert!(matches!(&refused, crate::EngineError::Disabled(cmd, _) if cmd == "diagnostics.kernel"), "{refused}");
|
||||
if !solidcraft_kernel::smlib_built() {
|
||||
let mut s = Session::default();
|
||||
let before = s.execute("diagnostics.kernel", &json!({})).unwrap();
|
||||
assert!(matches!(s.execute("diagnostics.kernel", &json!({"choice": "smlib"})), Err(crate::EngineError::Disabled(..))));
|
||||
assert_eq!(s.execute("diagnostics.kernel", &json!({})).unwrap(), before);
|
||||
}
|
||||
let r = s.execute("diagnostics.kernel", &json!({ "choice": "routed" })).unwrap();
|
||||
assert!(environment || r["choice"] == json!("routed"), "{r}");
|
||||
}
|
||||
|
||||
/// An unknown or mistyped choice is an error that changes nothing; no hostile value panics.
|
||||
@@ -143,6 +130,7 @@ fn diagnostics_kernel_refuses_unknown_choices() {
|
||||
json!("SMLIB"),
|
||||
json!(""),
|
||||
json!(huge),
|
||||
json!("routed"),
|
||||
json!("routed\0"),
|
||||
json!(5),
|
||||
json!(true),
|
||||
@@ -150,8 +138,8 @@ fn diagnostics_kernel_refuses_unknown_choices() {
|
||||
json!({"choice": "smlib"}),
|
||||
] {
|
||||
match s.execute("diagnostics.kernel", &json!({ "choice": bad })) {
|
||||
Err(crate::EngineError::Internal(..)) | Ok(_) => panic!("`{}` was not refused", bad.as_str().map_or(0, str::len)),
|
||||
Err(_) => {}
|
||||
Err(crate::EngineError::BadParams { .. }) => {}
|
||||
other => panic!("`{}` was not refused: {:?}", bad.as_str().map_or(0, str::len), other.map_err(|e| e.to_string())),
|
||||
}
|
||||
}
|
||||
for v in crate::tests::hostile_values() {
|
||||
|
||||
@@ -214,7 +214,7 @@ pub use prism_curve::{PlanarPiece, PlanarSpline};
|
||||
pub use prism_measure::{PrismSection, certified_prism_equivalent, certified_prism_section};
|
||||
pub use repair::{Hole, RebuiltFace, RefusedFace, RepairOptions, RepairReport, SewnGap, repair};
|
||||
pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew, sew_touching};
|
||||
/// Which kernel answers each operation, SMLib or solidcore (crates/smlib/src/route.rs).
|
||||
/// Which kernel the process runs on, solidcore or SMLib (crates/smlib/src/route.rs).
|
||||
pub use solidcraft_smlib::route as kernel_route;
|
||||
pub use splitface::{SplitTool, split_body, split_faces, split_faces_with_map};
|
||||
pub use step::step_export;
|
||||
|
||||
@@ -53,7 +53,7 @@ impl Brep {
|
||||
/// Runs SMLib's validity check at `level`. A body that fails it is still `Ok`, with the
|
||||
/// failures in the report; `Err` means the check could not run.
|
||||
pub fn check(&self, level: CheckLevel) -> Result<CheckReport> {
|
||||
if route::Op::Check.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::check(self.core()?, level);
|
||||
}
|
||||
let mut buf = vec![0u8; 1 << 16];
|
||||
|
||||
@@ -174,7 +174,7 @@ fn corner_miss(m: Miss) -> Error {
|
||||
}
|
||||
|
||||
/// A corner set back: its vertex and the setback along each of its edges (the capi's
|
||||
/// `SetBack`; the routed entry points take no ease, which is 0).
|
||||
/// `SetBack`; the entry points take no ease, which is 0).
|
||||
struct SetBack {
|
||||
vertex: VertexId,
|
||||
distance: f64,
|
||||
|
||||
@@ -282,7 +282,7 @@ fn solidcore_case(name: &'static str) -> Got {
|
||||
})
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing).
|
||||
/// The case on the C++ kernel, called directly (whatever the choice).
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
use crate::ops::{pick_error, setback_error};
|
||||
@@ -726,7 +726,7 @@ fn the_first_distance_goes_on_the_face_along_first() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through blends routed to solidcore, as on C++: the box is
|
||||
/// Face and edge tags (`Brep::tagged`) through blends on solidcore, as on C++: the box is
|
||||
/// tagged on the C++ kernel, its solidcore body converted from that and tagged the same way, and
|
||||
/// the two results' histories must be the same.
|
||||
#[cfg(feature = "native")]
|
||||
|
||||
@@ -188,7 +188,7 @@ fn solidcore_case(name: &'static str) -> Option<Measured> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing): what it measured.
|
||||
/// The case on the C++ kernel, called directly (whatever the choice): what it measured.
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Option<Measured> {
|
||||
use crate::{Brep, ffi};
|
||||
@@ -390,7 +390,7 @@ fn a_failed_closest_point_counts_as_off_the_piece() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through operations routed to solidcore, as on C++: each
|
||||
/// Face and edge tags (`Brep::tagged`) through operations on solidcore, as on C++: each
|
||||
/// operand is tagged on the C++ kernel, its solidcore body converted from that and tagged the same
|
||||
/// way there, and the two results' histories ([`Raw`], each in its own kernel's face and edge
|
||||
/// order) must be the same.
|
||||
|
||||
@@ -96,7 +96,7 @@ fn round_trips(name: &str) {
|
||||
fn matches_cpp(name: &str) {
|
||||
use crate::flat::{TOLERANCE, VERTEX_TOLERANCE, WIRE_EDGE_NURBS_FORM};
|
||||
use crate::{Brep, ffi};
|
||||
// built by the C API whatever `Op::Primitive` routes to
|
||||
// built by the C API whichever kernel is in force
|
||||
crate::init().unwrap();
|
||||
let cpp = match name {
|
||||
"box" => ffi::make_box(&[1.0, -2.0, 3.0], &[10.0, 20.0, 30.0]),
|
||||
@@ -509,7 +509,7 @@ mod cpp_round_trip {
|
||||
Brep::made(ffi::ops::boolean(a.cpp().unwrap(), b.cpp().unwrap(), ffi::ops::BOOLEAN_SUBTRACT).unwrap())
|
||||
}
|
||||
|
||||
/// `f` with the sweeps on the C++ kernel, whatever the route.
|
||||
/// `f` with the sweeps on the C++ kernel, whatever the choice.
|
||||
fn on_cpp(f: impl FnOnce() -> crate::Result<Brep>) -> Brep {
|
||||
crate::init().unwrap();
|
||||
crate::sweeps::ON_CPP.with(|c| c.set(true));
|
||||
|
||||
@@ -109,8 +109,8 @@ fn check_refuses_only_an_unknown_naming() {
|
||||
name(NamingScheme::Caller).unwrap();
|
||||
}
|
||||
|
||||
/// The C++ kernel's body of the same primitive, built by its C API whatever `Op::Primitive`
|
||||
/// routes to: a body imported from solidcore's arrays is not the C++ kernel's own (its
|
||||
/// The C++ kernel's body of the same primitive, built by its C API whichever kernel is in
|
||||
/// force: a body imported from solidcore's arrays is not the C++ kernel's own (its
|
||||
/// `edge_face_distance` measures to each face's surface over its domain, which the import sets).
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp(name: &str) -> crate::Brep {
|
||||
|
||||
@@ -201,7 +201,7 @@ fn solidcore_case(name: &str) -> Got {
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing), on the input converted
|
||||
/// The case on the C++ kernel, called directly (whatever the choice), on the input converted
|
||||
/// from solidcore's.
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
@@ -429,7 +429,7 @@ fn the_body_stays_as_it_was() {
|
||||
assert_eq!(counts(&block).unwrap(), was);
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through a stitch and a merge routed to solidcore, as on
|
||||
/// Face and edge tags (`Brep::tagged`) through a stitch and a merge on solidcore, as on
|
||||
/// C++: the input is tagged on solidcore, converted to C++ and tagged the same way there.
|
||||
#[cfg(feature = "native")]
|
||||
mod history {
|
||||
|
||||
@@ -337,7 +337,7 @@ fn solidcore_case(name: &'static str) -> Got {
|
||||
})
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing).
|
||||
/// The case on the C++ kernel, called directly (whatever the choice).
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
use crate::ops::pick_error;
|
||||
@@ -698,7 +698,7 @@ fn an_l_sheet_thickens_as_on_cpp() {
|
||||
assert!(rel(volume(&core, 1e-8).unwrap(), 840.0) < 1e-9);
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through offsets routed to solidcore, as on C++: the box is
|
||||
/// Face and edge tags (`Brep::tagged`) through offsets on solidcore, as on C++: the box is
|
||||
/// tagged on the C++ kernel, its solidcore body converted from that and tagged the same way, and
|
||||
/// the two results' histories must be the same.
|
||||
#[cfg(feature = "native")]
|
||||
|
||||
@@ -143,7 +143,7 @@ fn surface(spec: &Spec) -> crate::ssi::Surface {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing), on the cube converted
|
||||
/// The case on the C++ kernel, called directly (whatever the choice), on the cube converted
|
||||
/// from solidcore's.
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
@@ -349,7 +349,7 @@ fn the_body_stays_as_it_was() {
|
||||
assert!(rel(volume(&b, 1e-8).unwrap(), 1000.0) < 1e-12);
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through a face split routed to solidcore, as on C++.
|
||||
/// Face and edge tags (`Brep::tagged`) through a face split on solidcore, as on C++.
|
||||
#[cfg(feature = "native")]
|
||||
mod history {
|
||||
use crate::{Brep, ffi};
|
||||
|
||||
@@ -176,7 +176,7 @@ fn solidcore_case(name: &str) -> Got {
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing).
|
||||
/// The case on the C++ kernel, called directly (whatever the choice).
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
use crate::{Brep, error, ffi};
|
||||
@@ -292,7 +292,7 @@ fn the_body_stays_as_it_was() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through a copy and a transform routed to solidcore, as on
|
||||
/// Face and edge tags (`Brep::tagged`) through a copy and a transform on solidcore, as on
|
||||
/// C++: the box is tagged on the C++ kernel, its solidcore body converted from that and tagged the
|
||||
/// same way, and the two results' histories must be the same.
|
||||
#[cfg(feature = "native")]
|
||||
|
||||
@@ -133,7 +133,7 @@ fn solidcore_case(name: &str) -> Got {
|
||||
judge(name, "solidcore", got)
|
||||
}
|
||||
|
||||
/// The case on the C++ kernel, called directly (whatever the routing), on the plate converted
|
||||
/// The case on the C++ kernel, called directly (whatever the choice), on the plate converted
|
||||
/// from solidcore's.
|
||||
#[cfg(feature = "native")]
|
||||
fn cpp_case(name: &str) -> Got {
|
||||
@@ -230,7 +230,7 @@ fn the_body_stays_as_it_was() {
|
||||
assert_eq!(counts(&b).unwrap(), was);
|
||||
}
|
||||
|
||||
/// Face and edge tags (`Brep::tagged`) through an untrim routed to solidcore, as on C++.
|
||||
/// Face and edge tags (`Brep::tagged`) through an untrim on solidcore, as on C++.
|
||||
#[cfg(feature = "native")]
|
||||
mod history {
|
||||
use crate::{Brep, ffi};
|
||||
|
||||
@@ -50,7 +50,7 @@ use crate::{Brep, Result, error, ffi, init};
|
||||
impl Brep {
|
||||
/// A cylinder about +Z: base centre, radius and height (mm, > 0).
|
||||
pub fn make_cylinder(origin: [f64; 3], radius: f64, height: f64) -> Result<Brep> {
|
||||
if crate::route::Op::Primitive.backend() == crate::route::Backend::Solidcore {
|
||||
if crate::route::backend() == crate::route::Backend::Solidcore {
|
||||
return crate::core::make_cylinder(origin, radius, height).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -59,7 +59,7 @@ impl Brep {
|
||||
|
||||
/// A sphere: centre and radius (mm, > 0).
|
||||
pub fn make_sphere(center: [f64; 3], radius: f64) -> Result<Brep> {
|
||||
if crate::route::Op::Primitive.backend() == crate::route::Backend::Solidcore {
|
||||
if crate::route::backend() == crate::route::Backend::Solidcore {
|
||||
return crate::core::make_sphere(center, radius).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -69,7 +69,7 @@ impl Brep {
|
||||
/// A cone or frustum about +Z: base centre, base radius (> 0), top radius (≥ 0, not the base
|
||||
/// radius) and height (> 0), in mm.
|
||||
pub fn make_cone(origin: [f64; 3], base_radius: f64, top_radius: f64, height: f64) -> Result<Brep> {
|
||||
if crate::route::Op::Primitive.backend() == crate::route::Backend::Solidcore {
|
||||
if crate::route::backend() == crate::route::Backend::Solidcore {
|
||||
return crate::core::make_cone(origin, base_radius, top_radius, height).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -78,17 +78,17 @@ impl Brep {
|
||||
|
||||
/// A ring torus about +Z: centre, major radius > minor radius > 0 (mm).
|
||||
pub fn make_torus(center: [f64; 3], major_radius: f64, minor_radius: f64) -> Result<Brep> {
|
||||
if crate::route::Op::Primitive.backend() == crate::route::Backend::Solidcore {
|
||||
if crate::route::backend() == crate::route::Backend::Solidcore {
|
||||
return crate::core::make_torus(center, major_radius, minor_radius).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
ffi::make_torus(¢er, major_radius, minor_radius).map(Brep::made).map_err(error)
|
||||
}
|
||||
|
||||
/// The flat arrays of this body that the mapping reads, from the kernel [`route::Op::Convert`]
|
||||
/// routes to.
|
||||
/// The flat arrays of this body that the mapping reads, from the kernel in force
|
||||
/// ([`crate::route::backend`]).
|
||||
pub fn arrays(&self) -> Result<Arrays> {
|
||||
match crate::route::Op::Convert.backend() {
|
||||
match crate::route::backend() {
|
||||
crate::route::Backend::Smlib => self.cpp_arrays(),
|
||||
crate::route::Backend::Solidcore => crate::core::to_arrays(self.core()?),
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ impl Brep {
|
||||
|
||||
/// Whether SMLib finds this body one closed, manifold solid.
|
||||
pub fn is_manifold_solid(&self) -> Result<bool> {
|
||||
if crate::route::Op::Measure.backend() == crate::route::Backend::Solidcore {
|
||||
if crate::route::backend() == crate::route::Backend::Solidcore {
|
||||
return crate::core::is_manifold_solid(self.core()?);
|
||||
}
|
||||
ffi::flat::is_manifold_solid(self.cpp()?).map_err(error)
|
||||
|
||||
@@ -650,9 +650,9 @@ mod analytic;
|
||||
mod tests;
|
||||
|
||||
impl crate::Brep {
|
||||
/// A body of the kernel [`crate::route::Op::Convert`] routes to, from flat arrays.
|
||||
/// A body of the kernel in force ([`crate::route::backend`]), from flat arrays.
|
||||
pub(crate) fn from_arrays(a: &Arrays) -> crate::Result<crate::Brep> {
|
||||
match crate::route::Op::Convert.backend() {
|
||||
match crate::route::backend() {
|
||||
crate::route::Backend::Smlib => crate::flat::Flat::from_arrays(a)?.import(),
|
||||
crate::route::Backend::Solidcore => crate::core::from_arrays(a).map(crate::Brep::made_core),
|
||||
}
|
||||
|
||||
+14
-11
@@ -217,7 +217,7 @@ pub fn set_dump_dir(dir: Option<&Path>) -> Result<()> {
|
||||
let c = dir.map(c_path).transpose()?;
|
||||
core::dump::set_dir(dir.map(Path::to_path_buf));
|
||||
// A build or a run on solidcore alone never starts the C++ kernel to tell it.
|
||||
if !route::any_on_cpp() {
|
||||
if route::backend() != route::Backend::Smlib {
|
||||
return Ok(());
|
||||
}
|
||||
match init() {
|
||||
@@ -232,7 +232,7 @@ pub fn current_call() -> Option<String> {
|
||||
if let Some(call) = core::dump::current() {
|
||||
return Some(call);
|
||||
}
|
||||
if !route::any_on_cpp() {
|
||||
if route::backend() != route::Backend::Smlib {
|
||||
return None;
|
||||
}
|
||||
let mut buf = [0u8; 512];
|
||||
@@ -242,8 +242,11 @@ pub fn current_call() -> Option<String> {
|
||||
}
|
||||
|
||||
/// A B-rep in either kernel ([`route`]). Immutable: operations return new bodies. A body made
|
||||
/// by one kernel is converted for the other the first time an operation routed there needs it,
|
||||
/// through the flat arrays both understand, and keeps both.
|
||||
/// by one kernel is converted for the other the first time an operation there needs it, through the
|
||||
/// flat arrays both understand, and keeps both. A process has one kernel at a time, and this stays
|
||||
/// because that kernel can change under a live body: changing the preference or `diagnostics.kernel`
|
||||
/// leaves bodies already computed as they are, so the next operation on one needs it on the new
|
||||
/// kernel. The parity tests take solidcore's bodies to the C++ kernel the same way.
|
||||
pub struct Brep {
|
||||
cpp: OnceLock<Result<ffi::Handle>>,
|
||||
core: OnceLock<Result<core::Body>>,
|
||||
@@ -306,7 +309,7 @@ impl Brep {
|
||||
|
||||
/// An axis-aligned box from its lower corner, with positive edge lengths (mm).
|
||||
pub fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Brep> {
|
||||
if route::Op::Primitive.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::make_box(origin, size).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -315,7 +318,7 @@ impl Brep {
|
||||
|
||||
/// Reads a native SMLib `.smb` file.
|
||||
pub fn read(path: &Path, ascii: bool) -> Result<Brep> {
|
||||
if route::Op::File.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
c_path(path)?;
|
||||
return core::file::read(path, ascii).map(Brep::made_core);
|
||||
}
|
||||
@@ -325,7 +328,7 @@ impl Brep {
|
||||
|
||||
/// Writes this body to a native SMLib `.smb` file.
|
||||
pub fn write(&self, path: &Path, ascii: bool) -> Result<()> {
|
||||
if route::Op::File.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
c_path(path)?;
|
||||
return core::file::write(self.core()?, path, ascii);
|
||||
}
|
||||
@@ -334,7 +337,7 @@ impl Brep {
|
||||
|
||||
/// A deep copy.
|
||||
pub fn try_clone(&self) -> Result<Brep> {
|
||||
if route::Op::Transform.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::transform::copy(self.core()?).map(Brep::made_core);
|
||||
}
|
||||
ffi::copy(self.cpp()?).map(Brep::made).map_err(error)
|
||||
@@ -345,7 +348,7 @@ impl Brep {
|
||||
/// mirror it, a non-uniform scale or a shear turns its geometry into NURBS, tolerances scale
|
||||
/// with it.
|
||||
pub fn transformed(&self, matrix: &[[f64; 4]; 4]) -> Result<Brep> {
|
||||
if route::Op::Transform.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::transform::transform(self.core()?, matrix).map(Brep::made_core);
|
||||
}
|
||||
ffi::transform(self.cpp()?, matrix).map(Brep::made).map_err(error)
|
||||
@@ -353,7 +356,7 @@ impl Brep {
|
||||
|
||||
/// The enclosed volume (mm³) at `relative_accuracy` (SMLib clamps it to [1e-8, 1e-1]).
|
||||
pub fn volume(&self, relative_accuracy: f64) -> Result<f64> {
|
||||
if route::Op::Measure.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::volume(self.core()?, relative_accuracy);
|
||||
}
|
||||
ffi::volume(self.cpp()?, relative_accuracy).map_err(error)
|
||||
@@ -361,7 +364,7 @@ impl Brep {
|
||||
|
||||
/// Face, edge and vertex counts.
|
||||
pub fn counts(&self) -> Result<[usize; 3]> {
|
||||
if route::Op::Measure.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::counts(self.core()?);
|
||||
}
|
||||
let c = ffi::counts(self.cpp()?).map_err(error)?;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{Brep, Result, core, error, ffi, route};
|
||||
|
||||
/// Whether the Measure family runs on solidcore ([`route`]).
|
||||
fn on_core() -> bool {
|
||||
route::Op::Measure.backend() == route::Backend::Solidcore
|
||||
route::backend() == route::Backend::Solidcore
|
||||
}
|
||||
|
||||
/// The accuracy [`Brep::mass_properties`] and [`Brep::area`] use unless told otherwise: the
|
||||
|
||||
+18
-18
@@ -102,7 +102,7 @@ impl Brep {
|
||||
/// as one body, counted by [`Brep::solid_count`]. Tagged operands ([`Brep::tagged`]) give a
|
||||
/// result whose [`Brep::history`] says where each face and edge came from.
|
||||
pub fn boolean(&self, other: &Brep, op: BooleanOp) -> Result<Option<Brep>> {
|
||||
if route::Op::Boolean.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::boolean::boolean(self.core()?, other.core()?, op).map(|r| r.map(Brep::made_core));
|
||||
}
|
||||
init()?;
|
||||
@@ -123,7 +123,7 @@ impl Brep {
|
||||
/// tagged each with its own number ([`Brep::tagged`]) give a result whose [`Brep::history`]
|
||||
/// names them. At least one body and at most [`MAX_UNION_BREPS`].
|
||||
pub fn union_all(breps: &[&Brep]) -> Result<Brep> {
|
||||
if route::Op::Boolean.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
if breps.is_empty() || breps.len() > MAX_UNION_BREPS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ impl Brep {
|
||||
/// as regions of that plane by `op`: `None` when the result is empty. The result's edges are
|
||||
/// the inputs' own curves, split where they cross.
|
||||
pub fn boolean2d(&self, other: &Brep, op: BooleanOp) -> Result<Option<Brep>> {
|
||||
if route::Op::Boolean.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::boolean::boolean2d(self.core()?, other.core()?, op).map(|r| r.map(Brep::made_core));
|
||||
}
|
||||
init()?;
|
||||
@@ -162,7 +162,7 @@ impl Brep {
|
||||
/// one body: `None` when the tool meets no face. Tagged operands give the pieces' history
|
||||
/// (each piece is a split of the face it was cut from).
|
||||
pub fn imprint(&self, tool: &Brep) -> Result<Option<Brep>> {
|
||||
if route::Op::Boolean.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::boolean::imprint(self.core()?, tool.core()?).map(|r| r.map(Brep::made_core));
|
||||
}
|
||||
init()?;
|
||||
@@ -175,7 +175,7 @@ impl Brep {
|
||||
|
||||
/// The number of separate solid regions (voids not counted).
|
||||
pub fn solid_count(&self) -> Result<usize> {
|
||||
if route::Op::Measure.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return crate::core::solid_count(self.core()?);
|
||||
}
|
||||
ffi::ops::solid_count(self.cpp()?).map(|n| usize::try_from(n).unwrap_or(0)).map_err(error)
|
||||
@@ -184,7 +184,7 @@ impl Brep {
|
||||
/// The edges nearest to `edges` (each within `tolerance`), exactly those, rounded
|
||||
/// at `radius`.
|
||||
pub fn fillet(&self, edges: &[[f64; 3]], tolerance: f64, radius: f64) -> Result<Brep> {
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return self.core_blend(Blend::Round, edges, &[radius], tolerance, &[], &[], core::blend::Corners::Filled);
|
||||
}
|
||||
self.pick_op(ffi::ops::Pick::Fillet, edges, tolerance, radius, false)
|
||||
@@ -194,7 +194,7 @@ impl Brep {
|
||||
/// an equal-distance chamfer `distance` back along each face (measured at each edge's
|
||||
/// midpoint), with bevelled corners.
|
||||
pub fn chamfer(&self, edges: &[[f64; 3]], tolerance: f64, distance: f64) -> Result<Brep> {
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return self.core_blend(Blend::Chamfer, edges, &[distance], tolerance, &[], &[], core::blend::Corners::Filled);
|
||||
}
|
||||
self.pick_op(ffi::ops::Pick::Chamfer, edges, tolerance, distance, false)
|
||||
@@ -206,7 +206,7 @@ impl Brep {
|
||||
if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.len() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return self.core_blend(kind, edges, sizes, tolerance, &[], &[], core::blend::Corners::Filled);
|
||||
}
|
||||
init()?;
|
||||
@@ -226,7 +226,7 @@ impl Brep {
|
||||
if edges.len() > MAX_POINTS || edges.is_empty() || sizes.len() != edges.len() {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::blend::blend_dropping(self.core()?, kind, edges, sizes, tolerance).map(|(b, d)| (Brep::made_core(b), d));
|
||||
}
|
||||
init()?;
|
||||
@@ -262,7 +262,7 @@ impl Brep {
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return self.core_blend(kind, edges, sizes, tolerance, corners, setbacks, core::blend::Corners::Filled);
|
||||
}
|
||||
init()?;
|
||||
@@ -298,7 +298,7 @@ impl Brep {
|
||||
{
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::blend::blend_holes(self.core()?, kind, edges, sizes, tolerance, corners, setbacks, core::blend::Corners::Open)
|
||||
.map(|(b, holes)| (Brep::made_core(b), holes));
|
||||
}
|
||||
@@ -320,7 +320,7 @@ impl Brep {
|
||||
/// `radii` over the position along the edge, 0 at its end nearest `start` and 1 at the other
|
||||
/// (the edge curve's parameter scaled to 0..1).
|
||||
pub fn fillet_variable(&self, edge: [f64; 3], tolerance: f64, start: [f64; 3], degree: usize, knots: &[f64], radii: &[f64]) -> Result<Brep> {
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::blend::fillet_variable(self.core()?, edge, tolerance, start, degree, knots, radii).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -340,7 +340,7 @@ impl Brep {
|
||||
if edges.is_empty() || edges.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Blend.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::blend::chamfer_sides(self.core()?, edges, tolerance, distance, second, first).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -357,7 +357,7 @@ impl Brep {
|
||||
/// whose walls would just meet (a hole exactly twice the wall from a side) is
|
||||
/// [`Error::WallsTouch`].
|
||||
pub fn shell(&self, open_faces: &[[f64; 3]], tolerance: f64, thickness: f64) -> Result<Brep> {
|
||||
if route::Op::Offset.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::offset::shell(self.core()?, open_faces, tolerance, thickness).map(Brep::made_core);
|
||||
}
|
||||
self.pick_op(ffi::ops::Pick::Shell, open_faces, tolerance, thickness, true)
|
||||
@@ -368,7 +368,7 @@ impl Brep {
|
||||
/// faces around them extended or trimmed to meet them. Faces moved in past all of the body
|
||||
/// behind them are [`Error::NoRoom`].
|
||||
pub fn offset_faces(&self, faces: &[[f64; 3]], tolerance: f64, distance: f64) -> Result<Brep> {
|
||||
if route::Op::Offset.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::offset::offset_faces(self.core()?, faces, tolerance, distance).map(Brep::made_core);
|
||||
}
|
||||
self.pick_op(ffi::ops::Pick::OffsetFaces, faces, tolerance, distance, false)
|
||||
@@ -378,7 +378,7 @@ impl Brep {
|
||||
/// against them), or by half each way when `symmetric`, with ruled walls along its open
|
||||
/// edges. A closed solid is [`Error::Argument`].
|
||||
pub fn thicken(&self, thickness: f64, symmetric: bool) -> Result<Brep> {
|
||||
if route::Op::Offset.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::offset::thicken(self.core()?, thickness, symmetric).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -394,7 +394,7 @@ impl Brep {
|
||||
if keep.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::merge::merge_faces(self.core()?, keep, tolerance).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -418,7 +418,7 @@ impl Brep {
|
||||
if faces.is_empty() || faces.len() > MAX_POINTS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::offset::draft(self.core()?, faces, tolerance, neutral_origin, neutral_normal, pull, angle).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
|
||||
@@ -416,7 +416,7 @@ fn a_setback_corner_left_open_returns_its_hole() {
|
||||
let (open, holes) = b.blend_setback_open(Blend::Round, &edges, &[5.0; 3], &[[60.0, 40.0, 20.0]], &[8.0], 0.1).unwrap();
|
||||
// The call is a profiler span like every other, timed by the C API.
|
||||
let report = mark.report();
|
||||
let on_cpp = crate::route::Op::Blend.backend() == crate::route::Backend::Smlib;
|
||||
let on_cpp = crate::route::backend() == crate::route::Backend::Smlib;
|
||||
assert!(!on_cpp || report.rows.iter().any(|r| r.category == "smlib" && r.name == "smlib_blend_setback_open"), "{report:?}");
|
||||
shot(&open, "setback_open");
|
||||
assert_eq!(open.counts().unwrap(), [9, 30, 22]);
|
||||
|
||||
+51
-364
@@ -1,23 +1,18 @@
|
||||
//! Which kernel answers each operation: solidcore, the Rust port of SMLib, unless the C++ SMLib
|
||||
//! fork is asked for.
|
||||
//! Which kernel this process runs on: solidcore, the Rust port of SMLib, unless the C++ SMLib
|
||||
//! fork is asked for. One kernel answers every operation.
|
||||
//!
|
||||
//! [`Op::route`] is the routing table, and every family on it runs on solidcore. The C++ kernel is
|
||||
//! an opt-in: for a test, a rig or a user who wants the old behaviour. A call that fails on solidcore
|
||||
//! fails, and a path solidcore has no answer for says it is not supported: there is no fallback to
|
||||
//! C++ at run time, which would hide solidcore's bugs.
|
||||
//! The C++ kernel is an opt-in: for a test, a rig or a user who wants the old behaviour. A call that
|
||||
//! fails on solidcore fails, and a path solidcore has no answer for says it is not supported: there is
|
||||
//! no fallback to the other kernel at run time, which would hide its bugs. Asking for the C++ kernel on
|
||||
//! a build without it (wasm included) stays on it too: every call answers [`crate::Error::NotBuilt`].
|
||||
//!
|
||||
//! `SOLIDCRAFT_KERNEL=smlib|solidcore` sends every operation to one kernel for the whole process
|
||||
//! (the rig, the test matrix); otherwise the preference ([`set_choice`]) does, and its default
|
||||
//! follows the table.
|
||||
//!
|
||||
//! `SOLIDCRAFT_KERNEL_ROUTES=<op>=<backend>[,…]` (for example `boolean=smlib,measure=smlib`) then
|
||||
//! moves single families on top of that choice, so the rig can score one family at a time
|
||||
//! ([`Overrides`]).
|
||||
//! `SOLIDCRAFT_KERNEL=smlib|solidcore` chooses for the whole process (the rig, the test matrix);
|
||||
//! otherwise the preference ([`set_choice`]) does, and it defaults to solidcore.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
/// A kernel an operation runs on.
|
||||
/// A kernel the process runs on.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
/// The C++ SMLib fork (tangency-labs/smlib) behind its C API.
|
||||
@@ -41,131 +36,9 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/// The operation families the table routes.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Op {
|
||||
/// A schema 4 record into a kernel body, and back (`from_record`, `to_native`).
|
||||
Convert,
|
||||
/// Boxes, cylinders, spheres, cones and tori.
|
||||
Primitive,
|
||||
/// Copies and affine transforms.
|
||||
Transform,
|
||||
/// Volume, area, mass properties, bounds, counts, distances.
|
||||
Measure,
|
||||
/// The body checker.
|
||||
Check,
|
||||
/// Booleans, unions, 2D booleans, imprints.
|
||||
Boolean,
|
||||
/// Fillets and chamfers, with setbacks and variable radii.
|
||||
Blend,
|
||||
/// Shell, face offset, thicken, sheet offset.
|
||||
Offset,
|
||||
/// Split, trim, untrim, extend, merge faces, draft, stitch.
|
||||
Split,
|
||||
/// Extrusions, revolutions, sweeps and lofts.
|
||||
Sweep,
|
||||
/// Surface/surface intersection, and the surfaces it takes.
|
||||
Ssi,
|
||||
/// Native `.smb` files, which both kernels read and write.
|
||||
File,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
/// Every family, in table order.
|
||||
pub const ALL: [Op; 12] = [
|
||||
Op::Convert,
|
||||
Op::Primitive,
|
||||
Op::Transform,
|
||||
Op::Measure,
|
||||
Op::Check,
|
||||
Op::Boolean,
|
||||
Op::Blend,
|
||||
Op::Offset,
|
||||
Op::Split,
|
||||
Op::Sweep,
|
||||
Op::Ssi,
|
||||
Op::File,
|
||||
];
|
||||
|
||||
/// The lower-case name `diagnostics.kernel` and `SOLIDCRAFT_KERNEL_ROUTES` spell it with.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Op::Convert => "convert",
|
||||
Op::Primitive => "primitive",
|
||||
Op::Transform => "transform",
|
||||
Op::Measure => "measure",
|
||||
Op::Check => "check",
|
||||
Op::Boolean => "boolean",
|
||||
Op::Blend => "blend",
|
||||
Op::Offset => "offset",
|
||||
Op::Split => "split",
|
||||
Op::Sweep => "sweep",
|
||||
Op::Ssi => "ssi",
|
||||
Op::File => "file",
|
||||
}
|
||||
}
|
||||
|
||||
/// Back from [`Op::id`]; anything else is `None`.
|
||||
pub fn from_id(id: &str) -> Option<Op> {
|
||||
Op::ALL.into_iter().find(|op| op.id() == id)
|
||||
}
|
||||
|
||||
/// Where this family sits in [`Op::ALL`].
|
||||
fn index(self) -> usize {
|
||||
Op::ALL.iter().position(|op| *op == self).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The routing table: the kernel each family runs on when nothing overrides it. Every family
|
||||
/// is on solidcore; the C++ kernel is asked for ([`Choice`], [`Overrides`]), never defaulted to.
|
||||
pub fn route(self) -> Backend {
|
||||
match self {
|
||||
Op::Convert
|
||||
| Op::Primitive
|
||||
| Op::Transform
|
||||
| Op::Measure
|
||||
| Op::Check
|
||||
| Op::Boolean
|
||||
| Op::Blend
|
||||
| Op::Offset
|
||||
| Op::Split
|
||||
| Op::Sweep
|
||||
| Op::Ssi
|
||||
| Op::File => Backend::Solidcore,
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel this family runs on now: `SOLIDCRAFT_KERNEL_ROUTES` for it, else
|
||||
/// `SOLIDCRAFT_KERNEL`, else the preference, else [`Op::route`].
|
||||
pub fn backend(self) -> Backend {
|
||||
#[cfg(test)]
|
||||
if ON_CPP.with(std::cell::Cell::get) {
|
||||
return Backend::Smlib;
|
||||
}
|
||||
self.backend_under(choice())
|
||||
}
|
||||
|
||||
/// The kernel this family runs on under `c`, whatever is in force now: what [`Op::backend`]
|
||||
/// answers once `c` is the choice (`SOLIDCRAFT_KERNEL_ROUTES` still applies on top).
|
||||
pub fn backend_under(self, c: Choice) -> Backend {
|
||||
self.backend_with(c, overrides())
|
||||
}
|
||||
|
||||
/// The kernel this family runs on under `c` with the overrides `o`, read from neither the
|
||||
/// environment nor the preference.
|
||||
pub fn backend_with(self, c: Choice, o: &Overrides) -> Backend {
|
||||
if let Some(b) = o.get(self) {
|
||||
return b;
|
||||
}
|
||||
match c {
|
||||
Choice::Routed => self.route(),
|
||||
Choice::Only(b) => b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Tests: this thread's operations run on the C++ kernel whatever the routing.
|
||||
/// Tests: this thread's operations run on the C++ kernel whatever the choice.
|
||||
static ON_CPP: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
/// Tests: how many times this thread has touched a C++ body, and called solidcore.
|
||||
static REACHED: std::cell::Cell<(u64, u64)> = const { std::cell::Cell::new((0, 0)) };
|
||||
@@ -191,7 +64,7 @@ pub(crate) fn take_reached() -> (u64, u64) {
|
||||
}
|
||||
|
||||
/// Tests that exercise the C++ kernel opt in: until the guard drops, this thread's operations run
|
||||
/// on it. Thread by thread, so that the other tests in the binary stay on the table's kernel. The
|
||||
/// on it. Thread by thread, so that the other tests in the binary stay on the process's kernel. The
|
||||
/// guard fails a test that never touched a C++ body, so a parity test can't quietly compare
|
||||
/// solidcore with itself.
|
||||
#[cfg(test)]
|
||||
@@ -213,159 +86,39 @@ pub(crate) fn on_cpp() -> impl Drop {
|
||||
Guard
|
||||
}
|
||||
|
||||
/// Whether any family runs on the C++ kernel now: nothing need start it otherwise.
|
||||
pub fn any_on_cpp() -> bool {
|
||||
Op::ALL.into_iter().any(|op| op.backend() == Backend::Smlib)
|
||||
}
|
||||
|
||||
/// What answers the operations: the routing table, or one kernel for all of them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum Choice {
|
||||
/// Each family on its kernel in [`Op::route`].
|
||||
#[default]
|
||||
Routed,
|
||||
/// Every family on this kernel.
|
||||
Only(Backend),
|
||||
}
|
||||
|
||||
impl Choice {
|
||||
/// `routed`, `smlib` or `solidcore`.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
Choice::Routed => "routed",
|
||||
Choice::Only(b) => b.id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Back from [`Choice::id`]; anything else is `None`.
|
||||
pub fn from_id(id: &str) -> Option<Choice> {
|
||||
if id == "routed" { Some(Choice::Routed) } else { Backend::from_id(id).map(Choice::Only) }
|
||||
/// The kernel that answers now: `SOLIDCRAFT_KERNEL`, else the preference, else solidcore.
|
||||
pub fn backend() -> Backend {
|
||||
#[cfg(test)]
|
||||
if ON_CPP.with(std::cell::Cell::get) {
|
||||
return Backend::Smlib;
|
||||
}
|
||||
choice()
|
||||
}
|
||||
|
||||
/// `SOLIDCRAFT_KERNEL`, read once. An unknown value is no override (and says so in the log).
|
||||
fn environment() -> Option<Choice> {
|
||||
static ENV: OnceLock<Option<Choice>> = OnceLock::new();
|
||||
fn environment() -> Option<Backend> {
|
||||
static ENV: OnceLock<Option<Backend>> = OnceLock::new();
|
||||
*ENV.get_or_init(|| {
|
||||
let v = std::env::var("SOLIDCRAFT_KERNEL").ok()?;
|
||||
let c = Choice::from_id(&v);
|
||||
let c = Backend::from_id(&v);
|
||||
if c.is_none() {
|
||||
log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL={v}: not smlib, solidcore or routed; ignored");
|
||||
log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL={v}: not smlib or solidcore; ignored");
|
||||
}
|
||||
c
|
||||
})
|
||||
}
|
||||
|
||||
/// Families moved to a kernel one by one, on top of the choice: `SOLIDCRAFT_KERNEL_ROUTES`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub struct Overrides([Option<Backend>; Op::ALL.len()]);
|
||||
|
||||
impl Overrides {
|
||||
/// No family moved.
|
||||
pub const NONE: Overrides = Overrides([None; Op::ALL.len()]);
|
||||
|
||||
/// Longest `SOLIDCRAFT_KERNEL_ROUTES` read; a longer one is ignored whole.
|
||||
pub const MAX_LEN: usize = 4096;
|
||||
/// Items read; the rest are ignored.
|
||||
pub const MAX_ITEMS: usize = 64;
|
||||
/// Longest item read; a longer one is ignored.
|
||||
const MAX_ITEM_LEN: usize = 64;
|
||||
|
||||
/// The kernel `op` is moved to, if any.
|
||||
pub fn get(&self, op: Op) -> Option<Backend> {
|
||||
self.0.get(op.index()).copied().flatten()
|
||||
}
|
||||
|
||||
/// Whether no family is moved.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.iter().all(Option::is_none)
|
||||
}
|
||||
|
||||
/// Every moved family and its kernel, in table order.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Op, Backend)> + '_ {
|
||||
Op::ALL.into_iter().filter_map(|op| self.get(op).map(|b| (op, b)))
|
||||
}
|
||||
|
||||
/// A `SOLIDCRAFT_KERNEL_ROUTES` value: comma-separated `<op>=<smlib|solidcore>`, spaces
|
||||
/// around each part allowed, a later item for the same family winning. Returns the overrides
|
||||
/// and a warning for each item ignored: an unknown family or kernel, an empty or overlong
|
||||
/// item, items past [`Overrides::MAX_ITEMS`], or a whole value longer
|
||||
/// than [`Overrides::MAX_LEN`].
|
||||
pub fn parse(text: &str) -> (Overrides, Vec<String>) {
|
||||
let mut o = Overrides::NONE;
|
||||
let mut warnings = Vec::new();
|
||||
if text.len() > Self::MAX_LEN {
|
||||
warnings.push(format!("{} bytes, longer than {}; ignored", text.len(), Self::MAX_LEN));
|
||||
return (o, warnings);
|
||||
}
|
||||
let mut items = text.split(',');
|
||||
for item in items.by_ref().take(Self::MAX_ITEMS) {
|
||||
let item = item.trim();
|
||||
let shown: String = item.chars().take(Self::MAX_ITEM_LEN).collect();
|
||||
if item.is_empty() {
|
||||
warnings.push("an empty item; ignored".into());
|
||||
continue;
|
||||
}
|
||||
if item.len() > Self::MAX_ITEM_LEN {
|
||||
warnings.push(format!("`{shown}…`: longer than {} bytes; ignored", Self::MAX_ITEM_LEN));
|
||||
continue;
|
||||
}
|
||||
let Some((op, backend)) = item.split_once('=') else {
|
||||
warnings.push(format!("`{shown}`: not <op>=<smlib|solidcore>; ignored"));
|
||||
continue;
|
||||
};
|
||||
let Some(op) = Op::from_id(op.trim()) else {
|
||||
warnings.push(format!("`{shown}`: no operation family `{}`; ignored", op.trim()));
|
||||
continue;
|
||||
};
|
||||
let Some(backend) = Backend::from_id(backend.trim()) else {
|
||||
warnings.push(format!("`{shown}`: `{}` is not smlib or solidcore; ignored", backend.trim()));
|
||||
continue;
|
||||
};
|
||||
if let Some(slot) = o.0.get_mut(op.index()) {
|
||||
*slot = Some(backend);
|
||||
}
|
||||
}
|
||||
if items.next().is_some() {
|
||||
warnings.push(format!("more than {} items; the rest ignored", Self::MAX_ITEMS));
|
||||
}
|
||||
(o, warnings)
|
||||
}
|
||||
}
|
||||
|
||||
/// `SOLIDCRAFT_KERNEL_ROUTES`, read once; each item it ignores says so in the log.
|
||||
pub fn overrides() -> &'static Overrides {
|
||||
static ENV: OnceLock<Overrides> = OnceLock::new();
|
||||
ENV.get_or_init(|| {
|
||||
let Some(v) = std::env::var_os("SOLIDCRAFT_KERNEL_ROUTES") else { return Overrides::NONE };
|
||||
let v = v.to_string_lossy();
|
||||
let (o, warnings) = Overrides::parse(&v);
|
||||
for w in warnings {
|
||||
log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL_ROUTES: {w}");
|
||||
}
|
||||
o
|
||||
})
|
||||
}
|
||||
|
||||
static PREFERENCE: AtomicU8 = AtomicU8::new(0);
|
||||
/// The preference: false for solidcore, true for the C++ kernel.
|
||||
static PREFERS_CPP: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// The choice in force: `SOLIDCRAFT_KERNEL` when it is set, else the preference.
|
||||
pub fn choice() -> Choice {
|
||||
environment().unwrap_or_else(|| match PREFERENCE.load(Ordering::Relaxed) {
|
||||
1 => Choice::Only(Backend::Smlib),
|
||||
2 => Choice::Only(Backend::Solidcore),
|
||||
_ => Choice::Routed,
|
||||
})
|
||||
pub fn choice() -> Backend {
|
||||
environment().unwrap_or_else(|| if PREFERS_CPP.load(Ordering::Relaxed) { Backend::Smlib } else { Backend::Solidcore })
|
||||
}
|
||||
|
||||
/// Sets the preference. `SOLIDCRAFT_KERNEL`, when set, still wins.
|
||||
pub fn set_choice(c: Choice) {
|
||||
let v = match c {
|
||||
Choice::Routed => 0,
|
||||
Choice::Only(Backend::Smlib) => 1,
|
||||
Choice::Only(Backend::Solidcore) => 2,
|
||||
};
|
||||
PREFERENCE.store(v, Ordering::Relaxed);
|
||||
pub fn set_choice(b: Backend) {
|
||||
PREFERS_CPP.store(b == Backend::Smlib, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Whether `SOLIDCRAFT_KERNEL` overrides the preference in this process.
|
||||
@@ -377,104 +130,38 @@ pub fn overridden() -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The two ids round trip; `routed`, the old default, is not one of them.
|
||||
#[test]
|
||||
fn ids_round_trip() {
|
||||
for c in [Choice::Routed, Choice::Only(Backend::Smlib), Choice::Only(Backend::Solidcore)] {
|
||||
assert_eq!(Choice::from_id(c.id()), Some(c));
|
||||
for b in [Backend::Smlib, Backend::Solidcore] {
|
||||
assert_eq!(Backend::from_id(b.id()), Some(b));
|
||||
}
|
||||
assert_eq!(Choice::from_id("truck"), None);
|
||||
}
|
||||
|
||||
/// Every family, native files too, is on solidcore unless the C++ kernel is asked for.
|
||||
#[test]
|
||||
fn the_table_puts_every_family_on_solidcore() {
|
||||
assert!(Op::ALL.iter().all(|op| op.route() == Backend::Solidcore));
|
||||
}
|
||||
|
||||
/// Each choice sends the families where it says, read without setting the process-wide
|
||||
/// preference: the other tests in this binary build bodies meanwhile, and one flipped to
|
||||
/// solidcore for an instant would build theirs there.
|
||||
#[test]
|
||||
fn each_choice_routes_every_family_without_touching_the_preference() {
|
||||
for op in Op::ALL {
|
||||
let none = &Overrides::NONE;
|
||||
assert_eq!(op.backend_with(Choice::Routed, none), op.route(), "{op:?}");
|
||||
assert_eq!(op.backend_with(Choice::Only(Backend::Smlib), none), Backend::Smlib, "{op:?}");
|
||||
assert_eq!(op.backend_with(Choice::Only(Backend::Solidcore), none), Backend::Solidcore, "{op:?}");
|
||||
assert_eq!(op.backend_under(Choice::Routed), op.backend_with(Choice::Routed, overrides()), "{op:?}");
|
||||
for other in ["routed", "truck", "SMLIB", ""] {
|
||||
assert_eq!(Backend::from_id(other), None, "{other}");
|
||||
}
|
||||
}
|
||||
|
||||
/// With nothing chosen the process is on solidcore. (Only solidcore is set here: the other tests
|
||||
/// in this binary build bodies meanwhile.)
|
||||
#[test]
|
||||
fn op_ids_are_the_lower_case_names_and_round_trip() {
|
||||
for op in Op::ALL {
|
||||
assert_eq!(op.id(), format!("{op:?}").to_lowercase());
|
||||
assert_eq!(Op::from_id(op.id()), Some(op));
|
||||
}
|
||||
assert_eq!(Op::from_id("Boolean"), None);
|
||||
assert_eq!(Op::from_id(""), None);
|
||||
}
|
||||
|
||||
/// Overrides move only the families they name, on top of any choice; read from a string, not
|
||||
/// the process's environment.
|
||||
#[test]
|
||||
fn overrides_move_single_families_on_top_of_the_choice() {
|
||||
let (o, warnings) = Overrides::parse("boolean=solidcore, measure = solidcore");
|
||||
assert!(warnings.is_empty(), "{warnings:?}");
|
||||
assert_eq!(o.iter().collect::<Vec<_>>(), [(Op::Measure, Backend::Solidcore), (Op::Boolean, Backend::Solidcore)]);
|
||||
for op in Op::ALL {
|
||||
let moved = matches!(op, Op::Boolean | Op::Measure);
|
||||
assert_eq!(op.backend_with(Choice::Routed, &o), Backend::Solidcore, "{op:?}");
|
||||
let want = if moved { Backend::Solidcore } else { Backend::Smlib };
|
||||
assert_eq!(op.backend_with(Choice::Only(Backend::Smlib), &o), want, "{op:?}");
|
||||
}
|
||||
// Back to SMLib under the table; the later item wins.
|
||||
let (o, _) = Overrides::parse("file=smlib");
|
||||
assert_eq!(Op::File.backend_with(Choice::Routed, &o), Backend::Smlib);
|
||||
assert_eq!(Op::Sweep.backend_with(Choice::Routed, &o), Backend::Solidcore);
|
||||
let (o, _) = Overrides::parse("check=solidcore,check=smlib");
|
||||
assert_eq!(Op::Check.backend_with(Choice::Only(Backend::Solidcore), &o), Backend::Smlib);
|
||||
assert_eq!(Op::Sweep.backend_with(Choice::Only(Backend::Solidcore), &o), Backend::Solidcore);
|
||||
assert!(Overrides::parse("").0.is_empty());
|
||||
}
|
||||
|
||||
/// Hostile values: every bad item is ignored with a warning, the good ones still apply, and
|
||||
/// nothing panics or reads past the caps.
|
||||
#[test]
|
||||
fn overrides_ignore_bad_items_with_a_warning() {
|
||||
let (o, w) = Overrides::parse("truck=solidcore,boolean=truck,,=,boolean,BOOLEAN=solidcore,blend=solidcore=x,sweep=solidcore");
|
||||
assert_eq!(o.iter().collect::<Vec<_>>(), [(Op::Sweep, Backend::Solidcore)]);
|
||||
assert_eq!(w.len(), 7, "{w:?}");
|
||||
let (o, w) = Overrides::parse(&"x".repeat(Overrides::MAX_LEN + 1));
|
||||
assert!(o.is_empty() && w.len() == 1, "{w:?}");
|
||||
let (o, w) = Overrides::parse(&format!("{}=solidcore,blend=solidcore", "a".repeat(200)));
|
||||
assert_eq!((o.get(Op::Blend), w.len()), (Some(Backend::Solidcore), 1), "{w:?}");
|
||||
let many = vec!["measure=solidcore"; Overrides::MAX_ITEMS].join(",") + ",blend=solidcore";
|
||||
let (o, w) = Overrides::parse(&many);
|
||||
assert_eq!((o.get(Op::Measure), o.get(Op::Blend), w.len()), (Some(Backend::Solidcore), None, 1), "{w:?}");
|
||||
for hostile in [",,,,", "\0=\0", "boolean=solidcore\0", "é=ü,ü", "=solidcore", "boolean="] {
|
||||
let (o, w) = Overrides::parse(hostile);
|
||||
assert!(o.is_empty() && !w.is_empty(), "{hostile:?}: {w:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The preference is what `backend` follows. Only a choice that routes every family as the
|
||||
/// table does is set here, so no other test's body changes kernel.
|
||||
#[test]
|
||||
fn the_preference_routes_unless_the_environment_overrides() {
|
||||
if overridden() || !overrides().is_empty() {
|
||||
fn the_default_is_solidcore() {
|
||||
if overridden() {
|
||||
return;
|
||||
}
|
||||
let harmless = [Choice::Only(Backend::Smlib), Choice::Only(Backend::Solidcore)]
|
||||
.into_iter()
|
||||
.find(|c| Op::ALL.iter().all(|op| op.backend_under(*c) == op.route()));
|
||||
if let Some(c) = harmless {
|
||||
set_choice(c);
|
||||
assert_eq!(choice(), c);
|
||||
assert!(Op::ALL.iter().all(|op| op.backend() == op.backend_under(c)));
|
||||
set_choice(Backend::Solidcore);
|
||||
assert_eq!((choice(), backend()), (Backend::Solidcore, Backend::Solidcore));
|
||||
}
|
||||
|
||||
/// The guard moves this thread alone onto the C++ kernel, and back when it drops.
|
||||
#[test]
|
||||
fn a_test_opts_in_to_the_cpp_kernel_thread_by_thread() {
|
||||
let before = backend();
|
||||
{
|
||||
let _cpp = on_cpp();
|
||||
reached_cpp();
|
||||
assert_eq!(backend(), Backend::Smlib);
|
||||
assert_eq!(choice(), before);
|
||||
}
|
||||
set_choice(Choice::Routed);
|
||||
assert_eq!(choice(), Choice::Routed);
|
||||
assert!(Op::ALL.iter().all(|op| op.backend() == op.route()));
|
||||
assert_eq!(backend(), before);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ impl Brep {
|
||||
/// outward where they close into a solid, else only made to agree across their shared edges;
|
||||
/// with whether the result is a closed solid.
|
||||
pub fn stitch(&self, tolerance: f64) -> Result<(Brep, bool)> {
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::merge::stitch(self.core()?, tolerance).map(|(b, closed)| (Brep::made_core(b), closed));
|
||||
}
|
||||
init()?;
|
||||
@@ -20,7 +20,7 @@ impl Brep {
|
||||
/// This sheet with every face moved onto its surface's offset by `distance` along its normal
|
||||
/// (negative: against it). A closed solid is [`crate::Error::Argument`].
|
||||
pub fn offset_sheet(&self, distance: f64) -> Result<Brep> {
|
||||
if route::Op::Offset.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::offset::offset_sheet(self.core()?, distance).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
|
||||
@@ -14,7 +14,7 @@ impl Brep {
|
||||
if faces.is_empty() || faces.len() > ffi::split::MAX_FACES || tools.is_empty() || tools.len() > ffi::split::MAX_TOOLS {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
let specs: Vec<&crate::ssi::Spec> = tools.iter().map(|t| t.spec()).collect();
|
||||
return core::split::split_faces(self.core()?, faces, &specs).map(|r| r.map(|(b, made)| (Brep::made_core(b), made)));
|
||||
}
|
||||
@@ -34,7 +34,7 @@ impl Brep {
|
||||
/// leaves both parts the whole body, with no face from the tool (tag `self` to tell); `None`
|
||||
/// when a side is left empty.
|
||||
pub fn split_body(&self, tool: &Surface) -> Result<Option<(Brep, Brep)>> {
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::split::split_body(self.core()?, tool.spec()).map(|r| r.map(|(b, f)| (Brep::made_core(b), Brep::made_core(f))));
|
||||
}
|
||||
init()?;
|
||||
@@ -51,7 +51,7 @@ impl Brep {
|
||||
/// normal and the part in front. A `sheet` that is no sheet, or a `face` it does not have, is
|
||||
/// [`Error::Argument`].
|
||||
pub fn split_body_by_sheet(&self, sheet: &Brep, face: usize) -> Result<Option<(Brep, Brep)>> {
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::split::split_body_by_sheet(self.core()?, sheet.core()?, face)
|
||||
.map(|r| r.map(|(b, f)| (Brep::made_core(b), Brep::made_core(f))));
|
||||
}
|
||||
|
||||
@@ -152,9 +152,9 @@ impl Spec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether intersections run on the C++ kernel ([`route::Op::Ssi`]).
|
||||
/// Whether intersections run on the C++ kernel ([`route::backend`]).
|
||||
fn ssi_on_cpp() -> bool {
|
||||
route::Op::Ssi.backend() == route::Backend::Smlib
|
||||
route::backend() == route::Backend::Smlib
|
||||
}
|
||||
|
||||
impl Surface {
|
||||
@@ -172,12 +172,12 @@ impl Surface {
|
||||
&self.spec
|
||||
}
|
||||
|
||||
/// A surface from its checked description. With a family that takes surfaces on the C++
|
||||
/// A surface from its checked description. On the C++
|
||||
/// kernel, its side is made now, so that what it refuses is refused here.
|
||||
fn made(spec: Spec) -> Result<Surface> {
|
||||
spec.check()?;
|
||||
let s = Surface { handle: OnceLock::new(), core: OnceLock::new(), spec };
|
||||
if ssi_on_cpp() || route::Op::Split.backend() == route::Backend::Smlib {
|
||||
if ssi_on_cpp() {
|
||||
s.handle()?;
|
||||
}
|
||||
Ok(s)
|
||||
|
||||
@@ -157,17 +157,17 @@ impl List {
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// Tests: this thread's sweeps run on the C++ kernel whatever the routing.
|
||||
/// Tests: this thread's sweeps run on the C++ kernel whatever the choice.
|
||||
pub(crate) static ON_CPP: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Whether the sweeps run on solidcore ([`route::Op::Sweep`]).
|
||||
/// Whether the sweeps run on solidcore ([`route::backend`]).
|
||||
fn on_core() -> bool {
|
||||
#[cfg(test)]
|
||||
if ON_CPP.with(std::cell::Cell::get) {
|
||||
return false;
|
||||
}
|
||||
route::Op::Sweep.backend() == route::Backend::Solidcore
|
||||
route::backend() == route::Backend::Solidcore
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
|
||||
@@ -18,7 +18,7 @@ fn box_end_to_end() {
|
||||
fn bad_arguments_are_errors() {
|
||||
assert_eq!(Brep::make_box([0.0; 3], [1.0, -1.0, 1.0]).unwrap_err(), Error::Argument);
|
||||
assert_eq!(Brep::make_box([f64::NAN, 0.0, 0.0], [1.0; 3]).unwrap_err(), Error::Argument);
|
||||
// each kernel refuses a bad box the same way, whichever one `Op::Primitive` routes to
|
||||
// each kernel refuses a bad box the same way, whichever one is in force
|
||||
init().unwrap();
|
||||
for size in [[1.0, -1.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, f64::NAN]] {
|
||||
assert_eq!(core::make_box([0.0; 3], size).err(), Some(Error::Argument), "solidcore: {size:?}");
|
||||
@@ -139,7 +139,7 @@ fn calls_from_many_threads_are_serialised() {
|
||||
#[test]
|
||||
fn without_native_every_call_says_so() {
|
||||
assert_eq!(init(), Err(Error::NotBuilt));
|
||||
// the C API's own box, whatever `Op::Primitive` routes to
|
||||
// the C API's own box, whichever kernel is in force
|
||||
assert_eq!(ffi::make_box(&[0.0; 3], &[1.0; 3]).map_err(error).err(), Some(Error::NotBuilt));
|
||||
// solidcore builds a box without SMLib; asking the C++ kernel about it says there is none
|
||||
let b = core::make_box([0.0; 3], [1.0; 3]).map(Brep::made_core).unwrap();
|
||||
@@ -266,7 +266,7 @@ fn without_the_cpp_kernel_every_entry_point_answers() {
|
||||
use crate::sweeps::{Affine, Curve, DraftCorner, Ends, SweepFrame};
|
||||
use crate::trim::{Continuation, Holes};
|
||||
use crate::{Blend, BooleanOp, ChamferSecond, CheckLevel};
|
||||
if route::Op::ALL.iter().any(|op| op.backend() != route::Backend::Solidcore) {
|
||||
if route::backend() != route::Backend::Solidcore {
|
||||
return;
|
||||
}
|
||||
fn says<T>(name: &str, r: Result<T>) -> Option<T> {
|
||||
@@ -411,7 +411,7 @@ fn every_call_is_a_profiler_span_timed_by_the_c_api() {
|
||||
let _cpp = route::on_cpp();
|
||||
solidcraft_profile::set_enabled(true);
|
||||
let mark = solidcraft_profile::mark();
|
||||
// the C API's own box, whatever `Op::Primitive` routes to
|
||||
// the C API's own box, whichever kernel is in force
|
||||
crate::init().unwrap();
|
||||
let b = ffi::make_box(&[0.0; 3], &[1.0; 3]).map(Brep::made).unwrap();
|
||||
let _ = b.volume(1e-7).unwrap();
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Brep {
|
||||
Holes::All => None,
|
||||
Holes::At(at) => Some(at),
|
||||
};
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::trim::untrim_face(self.core()?, point, tolerance, holes, outer, in_place).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -48,7 +48,7 @@ impl Brep {
|
||||
/// `how` says: SMLib's extended surface over the grown box, the face and its sides carrying
|
||||
/// the tags of those they continue. A sheet of another shape is [`crate::Error::Unsupported`].
|
||||
pub fn extend_sheet(&self, point: [f64; 3], tolerance: f64, distance: f64, how: Continuation) -> Result<Brep> {
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::trim::extend_sheet(self.core()?, point, tolerance, distance, how).map(Brep::made_core);
|
||||
}
|
||||
init()?;
|
||||
@@ -66,7 +66,7 @@ impl Brep {
|
||||
/// A point inside each face, in SMLib's face order: on the face's surface, inside its trims
|
||||
/// (`SmFace::CalculateAnInternalPoint`).
|
||||
pub fn face_points(&self) -> Result<Vec<[f64; 3]>> {
|
||||
if route::Op::Split.backend() == route::Backend::Solidcore {
|
||||
if route::backend() == route::Backend::Solidcore {
|
||||
return core::trim::face_points(self.core()?);
|
||||
}
|
||||
init()?;
|
||||
|
||||
@@ -585,15 +585,15 @@ static KERNEL_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
/// `None` when `SOLIDCRAFT_KERNEL` chose the other kernel: it wins over `set_choice`, and scoring
|
||||
/// the wrong kernel against this one's list says nothing.
|
||||
fn global_intersect_scores(backend: solidcraft_smlib::route::Backend) -> Option<Vec<Score>> {
|
||||
use solidcraft_smlib::route::{Choice, Op, set_choice};
|
||||
use solidcraft_smlib::route::{Backend, choice, set_choice};
|
||||
use solidcraft_smlib::ssi::{Method, intersect};
|
||||
let t0 = std::time::Instant::now();
|
||||
let cases = all_cases();
|
||||
let outcomes: Vec<Outcome> = {
|
||||
let _kernel = KERNEL_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
set_choice(Choice::Only(backend));
|
||||
if Op::Ssi.backend() != backend {
|
||||
set_choice(Choice::Routed);
|
||||
set_choice(backend);
|
||||
if choice() != backend {
|
||||
set_choice(Backend::Solidcore);
|
||||
eprintln!("skipped: SOLIDCRAFT_KERNEL chose the other kernel");
|
||||
return None;
|
||||
}
|
||||
@@ -608,7 +608,7 @@ fn global_intersect_scores(backend: solidcraft_smlib::route::Backend) -> Option<
|
||||
runner_output(r, &a, &b, t.elapsed().as_secs_f64() * 1e3)
|
||||
})
|
||||
.collect();
|
||||
set_choice(Choice::Routed);
|
||||
set_choice(Backend::Solidcore);
|
||||
outcomes
|
||||
};
|
||||
let ran = t0.elapsed();
|
||||
|
||||
+62
-21
@@ -34,7 +34,7 @@ use crate::viewport::NavMode;
|
||||
|
||||
pub const UNITS: [&str; 5] = ["mm", "cm", "m", "in", "ft"];
|
||||
/// The Geometry kernel preference's choices, as `diagnostics.kernel` spells them.
|
||||
pub const KERNELS: [&str; 3] = ["routed", "smlib", "solidcore"];
|
||||
pub const KERNELS: [&str; 2] = ["solidcore", "smlib"];
|
||||
pub const OPENSCAD_NAV: &str = "openscad";
|
||||
pub const TOUCHPAD_NAV: &str = "touchpad";
|
||||
/// A mouse button that can drag the camera.
|
||||
@@ -635,10 +635,11 @@ preference_settings! {
|
||||
/// Up/Down and the wheel over a focused value field step its plain number (`value_step`,
|
||||
/// #2320). Off by default: the keys move the text cursor and the wheel scrolls, as before.
|
||||
step_values: bool = false, PAGE_DESIGN => check_id("ui.step_values");
|
||||
/// Which geometry kernel answers (one of [`KERNELS`]): `routed` (each operation on the
|
||||
/// kernel the routing table gives it), `smlib` or `solidcore` for all. Applied through
|
||||
/// Which geometry kernel answers every operation (one of [`KERNELS`]): `solidcore`, the
|
||||
/// default, or `smlib`, the C++ kernel. A saved `routed`, the old default, reads as
|
||||
/// `solidcore` ([`migrate`]). Applied through
|
||||
/// `diagnostics.kernel`; `SOLIDCRAFT_KERNEL`, when set, wins.
|
||||
geometry_kernel: String = "routed".into(), PAGE_DESIGN;
|
||||
geometry_kernel: String = "solidcore".into(), PAGE_DESIGN;
|
||||
/// When SMLib's full check runs while features are computed (`diagnostics.validation`):
|
||||
/// `each_feature` (the default) checks the bodies each feature keeps once; `each_step`, as
|
||||
/// before, checks after every sweep, boolean and blend; `off` (experimental) never does.
|
||||
@@ -868,6 +869,8 @@ pub struct PrefsWindow {
|
||||
/// The choice `SOLIDCRAFT_KERNEL` forces when it overrides the preference (the command's
|
||||
/// `environment` and `choice`).
|
||||
pub(crate) kernel_environment: Option<String>,
|
||||
/// The saved kernel is one this build doesn't have (`smlib` without the C++ kernel): the page says so.
|
||||
pub(crate) kernel_unavailable: bool,
|
||||
/// The search text of the interface font list.
|
||||
font_filter: String,
|
||||
/// Search text on the Plugins page.
|
||||
@@ -1145,16 +1148,23 @@ pub fn apply(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
app.prefs_window.applied_profile = Some(profile);
|
||||
}
|
||||
// The geometry kernel, through its command (an unknown saved value is Routed).
|
||||
let kernel = KERNELS.iter().copied().find(|k| *k == app.preferences.geometry_kernel).unwrap_or("routed");
|
||||
// The geometry kernel, through its command (an unknown saved value is the default). `smlib` on
|
||||
// a build without it is refused: the preference page and the status line say so.
|
||||
let kernel = KERNELS.iter().copied().find(|k| *k == app.preferences.geometry_kernel).unwrap_or(KERNELS[0]);
|
||||
if app.prefs_window.applied_kernel.as_deref() != Some(kernel) {
|
||||
match app.session.execute("diagnostics.kernel", &json!({ "choice": kernel })) {
|
||||
Ok(v) => {
|
||||
let environment = v.get("environment").and_then(serde_json::Value::as_bool).unwrap_or(false);
|
||||
app.prefs_window.kernel_environment =
|
||||
environment.then(|| v.get("choice").and_then(serde_json::Value::as_str).unwrap_or_default().to_string());
|
||||
app.prefs_window.kernel_unavailable = false;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("geometry kernel preference `{kernel}`: {e}");
|
||||
app.prefs_window.kernel_unavailable = true;
|
||||
let message = app.tr("prefs.geometry_kernel_unavailable").to_string();
|
||||
app.set_status(message, true);
|
||||
}
|
||||
Err(e) => log::warn!("geometry kernel preference `{kernel}`: {e}"),
|
||||
}
|
||||
app.prefs_window.applied_kernel = Some(kernel.into());
|
||||
}
|
||||
@@ -2291,6 +2301,10 @@ pub fn migrate(mut p: Prefs) -> Prefs {
|
||||
p.orbit_around = OrbitAround::Cursor;
|
||||
p.orbit_cursor = false;
|
||||
}
|
||||
// `routed`, the old default, ran every operation on solidcore.
|
||||
if p.geometry_kernel == "routed" {
|
||||
p.geometry_kernel = KERNELS[0].into();
|
||||
}
|
||||
if !crate::space_mouse::drives_each_once(p.space_mouse_axis_source) {
|
||||
log::warn!("preferences: the 3D mouse axis assignment {:?} doesn't drive each axis once; using the default", p.space_mouse_axis_source);
|
||||
p.space_mouse_axis_source = crate::space_mouse::SAME_AXES;
|
||||
@@ -2365,6 +2379,10 @@ pub fn set(app: &mut SolveApp, p: &serde_json::Value) -> Result<serde_json::Valu
|
||||
{
|
||||
return Err("`validation` must be each_step, each_feature or off".into());
|
||||
}
|
||||
// Before `migrate`, which reads the old saved `routed` as the default: asked for now, it is refused.
|
||||
if p.get("geometry_kernel").is_some_and(|v| !v.as_str().is_some_and(|k| KERNELS.contains(&k))) {
|
||||
return Err(format!("`geometry_kernel` must be one of {}", KERNELS.join(", ")));
|
||||
}
|
||||
let new: Prefs = migrate(serde_json::from_value(cur).map_err(|e| e.to_string())?);
|
||||
if !UNITS.contains(&new.default_units.as_str()) {
|
||||
return Err(format!("units must be one of {}", UNITS.join(", ")));
|
||||
@@ -2479,34 +2497,57 @@ mod tests {
|
||||
|
||||
/// The Geometry kernel preference reaches the kernel through diagnostics.kernel, at startup
|
||||
/// and when it changes, and only a choice the command knows is accepted. `smlib` keeps the C++
|
||||
/// kernel reachable for every family, the behaviour from before solidcore was the default.
|
||||
/// (The kernel choice is process-wide and other tests start harnesses meanwhile, so this
|
||||
/// reads what `smlib` would route and only sets `solidcore`, where Routed runs everything.)
|
||||
/// kernel reachable, the behaviour from before solidcore was the default. (The kernel choice is
|
||||
/// process-wide and other tests start harnesses meanwhile, so `smlib` is only set on a build
|
||||
/// without it, where it is refused and nothing moves.)
|
||||
#[test]
|
||||
fn the_geometry_kernel_preference_applies_through_its_command() {
|
||||
use serde_json::json;
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.frames(1);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("routed"), "applied at startup");
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("solidcore"), "applied at startup");
|
||||
let r = h.app.session.execute("diagnostics.kernel", &json!({})).unwrap();
|
||||
let environment = (r["environment"] == json!(true)).then(|| r["choice"].as_str().unwrap_or_default().to_string());
|
||||
assert_eq!(h.app.prefs_window.kernel_environment, environment);
|
||||
assert!(h.call("ui.prefs", json!({"geometry_kernel": "truck"}))["error"].as_str().is_some_and(|e| e.contains("geometry_kernel")));
|
||||
assert_eq!(h.app.preferences.geometry_kernel, "routed");
|
||||
use solidcraft_engine::kernel::kernel_route::{Backend, Choice, Op, Overrides};
|
||||
for op in Op::ALL {
|
||||
assert_eq!(Choice::from_id("smlib").map(|c| op.backend_with(c, &Overrides::NONE)), Some(Backend::Smlib), "{op:?}");
|
||||
assert_eq!(op.backend_with(Choice::Routed, &Overrides::NONE), Backend::Solidcore, "{op:?}");
|
||||
}
|
||||
// `routed` is gone: a saved one reads as solidcore (below), asked for now it is refused.
|
||||
assert!(h.call("ui.prefs", json!({"geometry_kernel": "routed"}))["error"].as_str().is_some_and(|e| e.contains("geometry_kernel")));
|
||||
assert_eq!(h.app.preferences.geometry_kernel, "solidcore");
|
||||
assert_eq!(super::KERNELS, ["solidcore", "smlib"]);
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "solidcore"}));
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("solidcore"));
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "routed"}));
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("routed"));
|
||||
assert!(!h.app.prefs_window.kernel_unavailable);
|
||||
// Saved with the other preferences.
|
||||
let saved = serde_json::to_value(&h.app.preferences).unwrap();
|
||||
assert_eq!(saved["geometry_kernel"], json!("routed"));
|
||||
assert_eq!(saved["geometry_kernel"], json!("solidcore"));
|
||||
// On a build without the C++ kernel, asking for it says so: not a switch to the other one.
|
||||
if !solidcraft_engine::kernel::smlib_built() {
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "smlib"}));
|
||||
h.frames(2);
|
||||
assert!(h.app.prefs_window.kernel_unavailable);
|
||||
assert_eq!(h.app.preferences.geometry_kernel, "smlib");
|
||||
assert_ne!(h.app.session.execute("diagnostics.kernel", &json!({})).unwrap()["choice"], json!("smlib"));
|
||||
h.call("ui.prefs", json!({"geometry_kernel": "solidcore"}));
|
||||
h.frames(2);
|
||||
assert!(!h.app.prefs_window.kernel_unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
/// A saved `routed`, the old default, loads as `solidcore`, without an error; any other value
|
||||
/// this version doesn't know stays as it was saved and runs on the default, and nothing panics.
|
||||
#[test]
|
||||
fn a_saved_routed_kernel_loads_as_solidcore() {
|
||||
use serde_json::json;
|
||||
let read = |saved: serde_json::Value| super::migrate(serde_json::from_value::<super::Prefs>(saved).unwrap()).geometry_kernel;
|
||||
assert_eq!(read(json!({"geometry_kernel": "routed"})), "solidcore");
|
||||
assert_eq!(read(json!({})), "solidcore");
|
||||
assert_eq!(read(json!({"geometry_kernel": "smlib"})), "smlib");
|
||||
assert_eq!(read(json!({"geometry_kernel": "truck"})), "truck");
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.geometry_kernel = "truck".into();
|
||||
h.frames(2);
|
||||
assert_eq!(h.app.prefs_window.applied_kernel.as_deref(), Some("solidcore"));
|
||||
}
|
||||
|
||||
/// Preferences saved with a setting this version no longer has still load, every other
|
||||
|
||||
@@ -102,7 +102,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
// Under SOLIDCRAFT_KERNEL the control shows the environment's choice, disabled.
|
||||
let environment = app.prefs_window.kernel_environment.clone();
|
||||
ui.add_enabled_ui(environment.is_none(), |ui| {
|
||||
let options = [app.tr("prefs.geometry_kernel_routed"), "SMLib", "solidcore"];
|
||||
let options = ["solidcore", "SMLib"];
|
||||
let shown = environment.as_deref().unwrap_or(&app.preferences.geometry_kernel);
|
||||
let mut selected = KERNELS.iter().position(|k| *k == shown).unwrap_or(0);
|
||||
ui_kit::segmented(ui, &mut selected, &options, ui_kit::Segments { intrinsic: true, ..Default::default() });
|
||||
@@ -117,6 +117,9 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
if environment.is_some() {
|
||||
ui_kit::text(ui, app.tr("prefs.geometry_kernel_environment"), ui_kit::Text::Hint("geometry_kernel_environment"));
|
||||
}
|
||||
if app.prefs_window.kernel_unavailable {
|
||||
ui_kit::text(ui, app.tr("prefs.geometry_kernel_unavailable"), ui_kit::Text::Hint("geometry_kernel_unavailable"));
|
||||
}
|
||||
});
|
||||
// Where the files a design names may be: never a network location the user hasn't allowed.
|
||||
ui_kit::settings_section(ui, "network_locations", app.tr("prefs.network_locations_section"), |ui| {
|
||||
|
||||
Reference in New Issue
Block a user