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:
brett19
2026-10-31 21:48:04 -07:00
parent cd94f78f31
commit afb55d806b
34 changed files with 273 additions and 544 deletions
+4 -3
View File
@@ -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;
+14 -14
View File
@@ -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 上运行。"
}
}
+18 -12
View File
@@ -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> {
+1 -1
View File
@@ -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;
+30 -42
View File
@@ -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() {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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];
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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")]
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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));
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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")]
+2 -2
View File
@@ -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};
+2 -2
View File
@@ -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")]
+2 -2
View File
@@ -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};
+7 -7
View File
@@ -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(&center, 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()?),
}
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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)?;
+1 -1
View File
@@ -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
View File
@@ -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()?;
+1 -1
View File
@@ -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
View File
@@ -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);
}
}
+2 -2
View File
@@ -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()?;
+3 -3
View File
@@ -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))));
}
+4 -4
View File
@@ -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)
+3 -3
View File
@@ -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 {
+4 -4
View File
@@ -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();
+3 -3
View File
@@ -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()?;
+5 -5
View File
@@ -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
View File
@@ -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
+4 -1
View File
@@ -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| {