mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
A part the modelling kernel refuses is named, with what is wrong and what to do
SMLib's import says only that it refuses a body, so the kernel now keeps a structured
refusal with the body's SMLib cache (Body::smlib_refusal / smlib_refused: the first face SMLib
refuses on its own, found by sending each face alone, and its surface kind). A STEP import sends
each part's first instance as an operation would, notes the parts that need repair in the
import report, and the browser marks them ("VALVE_COVER_01: needs repair").
A feature dialog whose operation fails on such a part says, instead of the kernel's words and
an issue report: "Couldn't join to VALVE_COVER_01. One of its rounded faces can't be used for
modelling.", with Repair part (body.repair, declared as planned, also in the part's menu) and
Show face (selects and frames the face).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"en": {
|
||||
"Repair Part": "Repair Part",
|
||||
"browser.needs_repair": "{name}: needs repair",
|
||||
"cmd.body_repair": "Repair Part",
|
||||
"import_report.needs_repair_one": "1 part needs repair before it can be modelled on.",
|
||||
"import_report.needs_repair_other": "{count} parts need repair before they can be modelled on.",
|
||||
"refused.change": "Couldn't change {body}.",
|
||||
"refused.cut": "Couldn't cut {body}.",
|
||||
"refused.face_curved": "One of its curved faces can't be used for modelling.",
|
||||
"refused.face_flat": "One of its flat faces can't be used for modelling.",
|
||||
"refused.face_freeform": "One of its freeform faces can't be used for modelling.",
|
||||
"refused.face_rounded": "One of its rounded faces can't be used for modelling.",
|
||||
"refused.faces_apart": "Its faces don't meet closely enough to be used for modelling.",
|
||||
"refused.intersect": "Couldn't intersect with {body}.",
|
||||
"refused.join": "Couldn't join to {body}.",
|
||||
"refused.repair": "Repair part",
|
||||
"refused.show_face": "Show face"
|
||||
},
|
||||
"ar": {
|
||||
"Repair Part": "إصلاح الجزء",
|
||||
"browser.needs_repair": "{name}: يحتاج إلى إصلاح",
|
||||
"cmd.body_repair": "إصلاح الجزء",
|
||||
"import_report.needs_repair_one": "يحتاج جزء واحد إلى إصلاح قبل النمذجة عليه.",
|
||||
"import_report.needs_repair_other": "تحتاج {count} أجزاء إلى إصلاح قبل النمذجة عليها.",
|
||||
"refused.change": "تعذّر تغيير {body}.",
|
||||
"refused.cut": "تعذّر قطع {body}.",
|
||||
"refused.face_curved": "أحد أوجهه المنحنية لا يمكن استخدامه في النمذجة.",
|
||||
"refused.face_flat": "أحد أوجهه المستوية لا يمكن استخدامه في النمذجة.",
|
||||
"refused.face_freeform": "أحد أوجهه الحرة لا يمكن استخدامه في النمذجة.",
|
||||
"refused.face_rounded": "أحد أوجهه المستديرة لا يمكن استخدامه في النمذجة.",
|
||||
"refused.faces_apart": "أوجهه لا تلتقي بدقة كافية لاستخدامها في النمذجة.",
|
||||
"refused.intersect": "تعذّر التقاطع مع {body}.",
|
||||
"refused.join": "تعذّر الضم إلى {body}.",
|
||||
"refused.repair": "إصلاح الجزء",
|
||||
"refused.show_face": "إظهار الوجه"
|
||||
},
|
||||
"de": {
|
||||
"Repair Part": "Teil reparieren",
|
||||
"browser.needs_repair": "{name}: muss repariert werden",
|
||||
"cmd.body_repair": "Teil reparieren",
|
||||
"import_report.needs_repair_one": "1 Teil muss repariert werden, bevor daran modelliert werden kann.",
|
||||
"import_report.needs_repair_other": "{count} Teile müssen repariert werden, bevor daran modelliert werden kann.",
|
||||
"refused.change": "{body} konnte nicht geändert werden.",
|
||||
"refused.cut": "{body} konnte nicht geschnitten werden.",
|
||||
"refused.face_curved": "Eine seiner gekrümmten Flächen kann nicht zum Modellieren verwendet werden.",
|
||||
"refused.face_flat": "Eine seiner ebenen Flächen kann nicht zum Modellieren verwendet werden.",
|
||||
"refused.face_freeform": "Eine seiner Freiformflächen kann nicht zum Modellieren verwendet werden.",
|
||||
"refused.face_rounded": "Eine seiner gerundeten Flächen kann nicht zum Modellieren verwendet werden.",
|
||||
"refused.faces_apart": "Seine Flächen treffen nicht genau genug aufeinander, um damit zu modellieren.",
|
||||
"refused.intersect": "Schnittmenge mit {body} nicht möglich.",
|
||||
"refused.join": "Konnte nicht mit {body} vereinigt werden.",
|
||||
"refused.repair": "Teil reparieren",
|
||||
"refused.show_face": "Fläche zeigen"
|
||||
},
|
||||
"he": {
|
||||
"Repair Part": "תיקון חלק",
|
||||
"browser.needs_repair": "{name}: דרוש תיקון",
|
||||
"cmd.body_repair": "תיקון חלק",
|
||||
"import_report.needs_repair_one": "חלק אחד דורש תיקון לפני שאפשר למדל עליו.",
|
||||
"import_report.needs_repair_other": "{count} חלקים דורשים תיקון לפני שאפשר למדל עליהם.",
|
||||
"refused.change": "לא ניתן היה לשנות את {body}.",
|
||||
"refused.cut": "לא ניתן היה לחתוך את {body}.",
|
||||
"refused.face_curved": "אחת הפאות המעוקלות שלו אינה ניתנת לשימוש במידול.",
|
||||
"refused.face_flat": "אחת הפאות השטוחות שלו אינה ניתנת לשימוש במידול.",
|
||||
"refused.face_freeform": "אחת הפאות החופשיות שלו אינה ניתנת לשימוש במידול.",
|
||||
"refused.face_rounded": "אחת הפאות המעוגלות שלו אינה ניתנת לשימוש במידול.",
|
||||
"refused.faces_apart": "הפאות שלו אינן נפגשות בצמידות מספקת לשימוש במידול.",
|
||||
"refused.intersect": "לא ניתן היה לחתוך עם {body}.",
|
||||
"refused.join": "לא ניתן היה לחבר אל {body}.",
|
||||
"refused.repair": "תיקון חלק",
|
||||
"refused.show_face": "הצגת הפאה"
|
||||
},
|
||||
"ja": {
|
||||
"Repair Part": "パーツを修復",
|
||||
"browser.needs_repair": "{name}: 修復が必要",
|
||||
"cmd.body_repair": "パーツを修復",
|
||||
"import_report.needs_repair_one": "1 個のパーツはモデリングの前に修復が必要です。",
|
||||
"import_report.needs_repair_other": "{count} 個のパーツはモデリングの前に修復が必要です。",
|
||||
"refused.change": "{body} を変更できませんでした。",
|
||||
"refused.cut": "{body} をカットできませんでした。",
|
||||
"refused.face_curved": "曲面の 1 つがモデリングに使えません。",
|
||||
"refused.face_flat": "平らな面の 1 つがモデリングに使えません。",
|
||||
"refused.face_freeform": "自由曲面の 1 つがモデリングに使えません。",
|
||||
"refused.face_rounded": "丸みのある面の 1 つがモデリングに使えません。",
|
||||
"refused.faces_apart": "面どうしが十分に合っておらず、モデリングに使えません。",
|
||||
"refused.intersect": "{body} と交差できませんでした。",
|
||||
"refused.join": "{body} に結合できませんでした。",
|
||||
"refused.repair": "パーツを修復",
|
||||
"refused.show_face": "面を表示"
|
||||
},
|
||||
"ko": {
|
||||
"Repair Part": "부품 복구",
|
||||
"browser.needs_repair": "{name}: 복구 필요",
|
||||
"cmd.body_repair": "부품 복구",
|
||||
"import_report.needs_repair_one": "부품 1개는 모델링하기 전에 복구해야 합니다.",
|
||||
"import_report.needs_repair_other": "부품 {count}개는 모델링하기 전에 복구해야 합니다.",
|
||||
"refused.change": "{body}을(를) 변경하지 못했습니다.",
|
||||
"refused.cut": "{body}을(를) 자르지 못했습니다.",
|
||||
"refused.face_curved": "곡면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.face_flat": "평면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.face_freeform": "자유 곡면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.face_rounded": "둥근 면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.faces_apart": "면들이 충분히 맞물리지 않아 모델링에 사용할 수 없습니다.",
|
||||
"refused.intersect": "{body}과(와) 교차하지 못했습니다.",
|
||||
"refused.join": "{body}에 결합하지 못했습니다.",
|
||||
"refused.repair": "부품 복구",
|
||||
"refused.show_face": "면 보기"
|
||||
},
|
||||
"zh": {
|
||||
"Repair Part": "修复零件",
|
||||
"browser.needs_repair": "{name}:需要修复",
|
||||
"cmd.body_repair": "修复零件",
|
||||
"import_report.needs_repair_one": "1 个零件需要修复后才能建模。",
|
||||
"import_report.needs_repair_other": "{count} 个零件需要修复后才能建模。",
|
||||
"refused.change": "无法更改 {body}。",
|
||||
"refused.cut": "无法切割 {body}。",
|
||||
"refused.face_curved": "它的一个曲面无法用于建模。",
|
||||
"refused.face_flat": "它的一个平面无法用于建模。",
|
||||
"refused.face_freeform": "它的一个自由曲面无法用于建模。",
|
||||
"refused.face_rounded": "它的一个圆角面无法用于建模。",
|
||||
"refused.faces_apart": "它的面贴合得不够紧密,无法用于建模。",
|
||||
"refused.intersect": "无法与 {body} 求交。",
|
||||
"refused.join": "无法合并到 {body}。",
|
||||
"refused.repair": "修复零件",
|
||||
"refused.show_face": "显示面"
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,10 @@ use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("body.rename", "Rename Body", rename_body).icon("rename").params("body: its name, name: the new name (features that use the body follow)"),
|
||||
// The import and failed operations offer it for a part the modelling kernel refuses.
|
||||
planned!("body.repair", "Repair Part", "TBD")
|
||||
.icon("body")
|
||||
.params("planned: body: its name (a part the modelling kernel refuses: one its import marks as needing repair)"),
|
||||
CommandSpec::new("sketch.redefine", "Redefine Sketch Plane", redefine_sketch)
|
||||
.icon("plane")
|
||||
.params("sketch: id or name; plane: as sketch.create; frame?: as sketch.create; author_path?: exact owner occurrence ids; explicitly rebinds or freezes the plane while preserving geometry and dimension names"),
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct Body {
|
||||
pub(crate) record: std::sync::Arc<Brep>,
|
||||
/// The body in SMLib, built from `record` on first use (or kept from the operation that
|
||||
/// made it), shared by clones because they share the record.
|
||||
pub(crate) smlib: std::sync::Arc<std::sync::OnceLock<std::result::Result<solidcraft_smlib::Brep, String>>>,
|
||||
pub(crate) smlib: std::sync::Arc<std::sync::OnceLock<std::result::Result<solidcraft_smlib::Brep, Refusal>>>,
|
||||
/// Bounding-box diagonal of the vertices, measured when the body was made; 0 means "not
|
||||
/// measured, walk the geometry". Every kernel tolerance is a fraction of it.
|
||||
pub(crate) size: f64,
|
||||
@@ -379,6 +379,61 @@ pub(crate) fn empty_record() -> Brep {
|
||||
}
|
||||
}
|
||||
|
||||
/// Why SMLib refuses a body, and the face it refuses on its own when one does: SMLib's import
|
||||
/// says only that it refuses the body, so each face is then sent on its own to find which.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Refusal {
|
||||
/// The first face (in the body's face order) SMLib refuses on its own; `None` when it takes
|
||||
/// each face alone (the faces do not fit together) or the body has too many to try.
|
||||
pub face: Option<usize>,
|
||||
/// That face's surface.
|
||||
pub surface: Option<SurfaceKind>,
|
||||
/// SMLib's reason for the whole body, as the log words it.
|
||||
pub why: String,
|
||||
}
|
||||
|
||||
/// The kind of surface a face lies on, as a person modelling would name it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SurfaceKind {
|
||||
Plane,
|
||||
Cylinder,
|
||||
Cone,
|
||||
Sphere,
|
||||
Torus,
|
||||
/// A B-spline or NURBS surface, or a revolution of a spline.
|
||||
Freeform,
|
||||
}
|
||||
|
||||
/// Most faces sent into SMLib one by one to find the one it refuses.
|
||||
const MAX_REFUSAL_PROBES: usize = 20_000;
|
||||
|
||||
impl Refusal {
|
||||
/// The refusal of `record` for `error`, with the face SMLib refuses on its own.
|
||||
fn of(record: &Brep, error: &solidcraft_smlib::Error) -> Refusal {
|
||||
let _span = solidcraft_profile::span("kernel", "smlib_refusal_face");
|
||||
let alone = |f: usize| crate::surfaces::Parts::of(record, &[(f, false)]).and_then(|p| p.canonical()).map(|c| c.brep);
|
||||
let face = (record.faces.len() <= MAX_REFUSAL_PROBES)
|
||||
.then(|| (0..record.faces.len()).find(|&f| alone(f).is_ok_and(|r| solidcraft_smlib::Brep::from_record(&r, true).is_err())))
|
||||
.flatten();
|
||||
let surface = face.and_then(|f| record.faces.get(f)).map(|f| SurfaceKind::of(&f.surface));
|
||||
Refusal { face, surface, why: error.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl SurfaceKind {
|
||||
fn of(surface: &solidcraft_geom::brep::Surface) -> SurfaceKind {
|
||||
use solidcraft_geom::brep::Surface as S;
|
||||
match surface {
|
||||
S::Plane { .. } | S::PlaneThrough { .. } => SurfaceKind::Plane,
|
||||
S::Cylinder { .. } => SurfaceKind::Cylinder,
|
||||
S::Cone { .. } => SurfaceKind::Cone,
|
||||
S::Sphere { .. } => SurfaceKind::Sphere,
|
||||
S::Torus { .. } => SurfaceKind::Torus,
|
||||
S::Spline { .. } | S::RationalSpline { .. } | S::PlacedRevolution { .. } => SurfaceKind::Freeform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// A body with `record` and nothing else set.
|
||||
fn bare(record: Brep, smlib: Option<solidcraft_smlib::Brep>, mesh: Option<std::sync::Arc<crate::meshbody::TriMesh>>) -> Body {
|
||||
@@ -433,9 +488,27 @@ impl Body {
|
||||
pub fn smlib(&self) -> Result<&solidcraft_smlib::Brep> {
|
||||
self.require_brep("an SMLib operation")?;
|
||||
self.smlib
|
||||
.get_or_init(|| solidcraft_smlib::Brep::from_record(&self.record, true).map_err(|e| e.to_string()))
|
||||
.get_or_init(|| solidcraft_smlib::Brep::from_record(&self.record, true).map_err(|e| Refusal::of(&self.record, &e)))
|
||||
.as_ref()
|
||||
.map_err(|e| KernelError::Failed(format!("SMLib: {e}")))
|
||||
.map_err(|r| KernelError::Failed(format!("SMLib: {}", r.why)))
|
||||
}
|
||||
|
||||
/// Why SMLib refuses the body, if it does: the body is sent into SMLib as an operation
|
||||
/// would send it (and kept there for the operations to come). `None` for a body SMLib takes,
|
||||
/// and for a mesh body (no operation sends one).
|
||||
pub fn smlib_refusal(&self) -> Option<&Refusal> {
|
||||
if self.is_mesh() {
|
||||
return None;
|
||||
}
|
||||
let _ = self.smlib();
|
||||
self.smlib_refused()
|
||||
}
|
||||
|
||||
/// Why SMLib refused the body when it was last sent there (by an operation, or by
|
||||
/// [`Self::smlib_refusal`]); `None` when it took it or the body was not sent yet. Sends
|
||||
/// nothing.
|
||||
pub fn smlib_refused(&self) -> Option<&Refusal> {
|
||||
self.smlib.get().and_then(|r| r.as_ref().err())
|
||||
}
|
||||
|
||||
/// The very same body (shared data), not just an equal-looking one. Its history (how an
|
||||
|
||||
@@ -159,7 +159,9 @@ pub use blend::{
|
||||
};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
pub use body::{Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint};
|
||||
pub use body::{
|
||||
Body, CapSignature, DISPLAY_ANGLE, DisplayShare, EdgeInfo, FaceInfo, FacePaint, FaceSource, MeshTolerance, Paint, Refusal, SurfaceKind,
|
||||
};
|
||||
pub use boundary::{Boundary, boundary_patch};
|
||||
pub use boundary_network::{BoundaryCurve, CurveSource, MAX_CHAIN_CURVES, MAX_EDGE_PICKS, boundary_join, boundary_surface};
|
||||
pub use bridge::{Frame, blend_between, bridge, chain, chain_closes, frames, frames_across, sample_grid};
|
||||
@@ -208,7 +210,8 @@ pub use sew::{EdgeSpec, FaceSpec, SurfSpec, sew, sew_touching};
|
||||
pub use splitface::{SplitTool, split_body, split_faces, split_faces_with_map};
|
||||
pub use step::step_export;
|
||||
pub use step_in::{
|
||||
ImportIssue, ImportReport, ImportedBody, IssueKind, StepImport, StepNode, StepReading, step_cache_alive, step_import, step_import_shared, step_orientation_errors, step_validate,
|
||||
ImportIssue, ImportReport, ImportedBody, IssueKind, StepImport, StepNode, StepReading, step_cache_alive, step_import, step_import_shared,
|
||||
step_orientation_errors, step_validate,
|
||||
};
|
||||
pub use step_out::{ExportBody, ExportProduct, StepHeader, step_export_bodies, step_export_products};
|
||||
pub use surface_extend::{ExtendLimit, ExtendMode, ExtendOptions, extend_surface};
|
||||
|
||||
@@ -107,6 +107,8 @@ pub enum IssueKind {
|
||||
OpenBody,
|
||||
/// Faces of the part could not be meshed and are not shown.
|
||||
FacesNotShown,
|
||||
/// SMLib refuses the part: modelling operations on it fail until it is repaired.
|
||||
NeedsRepair,
|
||||
/// Something was repaired on the way (edges split at a pole, faces turned, gaps covered).
|
||||
Repaired,
|
||||
/// The file as a whole: its assembly structure, its limits, its entity ids.
|
||||
@@ -288,6 +290,8 @@ struct Reader {
|
||||
visits: usize,
|
||||
/// Items whose meshing was checked.
|
||||
unmeshed_checked: HashSet<u64>,
|
||||
/// Items sent into SMLib as an operation would send them, to say which need repair.
|
||||
modelling_checked: HashSet<u64>,
|
||||
bodies: Vec<ImportedBody>,
|
||||
/// Solids the walk found, in file order, to be converted into bodies afterwards (so the
|
||||
/// conversion, the long part, can be done a piece at a time: [`StepReading`]).
|
||||
@@ -329,6 +333,7 @@ impl Reader {
|
||||
node_items: HashSet::new(),
|
||||
visits: 0,
|
||||
unmeshed_checked: HashSet::new(),
|
||||
modelling_checked: HashSet::new(),
|
||||
bodies: Vec::new(),
|
||||
plan: Vec::new(),
|
||||
batches: Vec::new(),
|
||||
@@ -868,6 +873,15 @@ impl Reader {
|
||||
}
|
||||
None => body,
|
||||
};
|
||||
// A part SMLib refuses cannot be modelled on: found now (its first instance,
|
||||
// sent as an operation would send it), not at the first operation.
|
||||
if self.modelling_checked.insert(item)
|
||||
&& let Some(refused) = body.smlib_refusal()
|
||||
{
|
||||
let at = refused.face.map(|f| format!(" (face {})", f + 1)).unwrap_or_default();
|
||||
let msg = format!("solid '{name}': needs repair before it can be modelled: {}{at}", refused.why);
|
||||
self.note(IssueKind::NeedsRepair, Some(item), usize::from(refused.face.is_some()), msg);
|
||||
}
|
||||
self.bodies.push(ImportedBody {
|
||||
name,
|
||||
body,
|
||||
|
||||
@@ -1982,12 +1982,13 @@ impl Entry {
|
||||
key: b.name.clone(),
|
||||
row_key: format!("b:{world}"),
|
||||
patterns: None,
|
||||
label: b.name.clone(),
|
||||
// A part the modelling kernel refuses says so (Repair Part is in its menu).
|
||||
label: if b.body.smlib_refused().is_some() { crate::i18n::format("browser.needs_repair", &[("name", &b.name)]) } else { b.name.clone() },
|
||||
// An open shell is a surface body, not a solid.
|
||||
icon: if b.is_surface() { "surface" } else { "body" },
|
||||
visible: !app.session.visibility.hidden_bodies.contains(&world),
|
||||
selected: app.session.selection.iter().any(|s| matches!(s, Sel::Body { name } if name == &world)),
|
||||
color: None,
|
||||
color: b.body.smlib_refused().map(|_| Tokens::get().warning),
|
||||
locked: app.session.visibility.locked_bodies.contains(&world),
|
||||
sel: Some(Sel::Body { name: world.clone() }),
|
||||
target: Some(Target::Body { name: world.clone() }),
|
||||
|
||||
@@ -668,6 +668,11 @@ fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec<Item> {
|
||||
v.push(act("ui.findBrowser", "Find in Browser", "list").with(json!({ "body": b })));
|
||||
}
|
||||
v.push(act("ui.findTimeline", "Find in Timeline", "rollback").with(json!({ "body": b })));
|
||||
// A part the modelling kernel refuses can be repaired.
|
||||
if crate::open_meshing::session_world(&app.session).body(&b).is_some_and(|m| m.body.smlib_refused().is_some()) {
|
||||
v.push(Item::sep());
|
||||
v.push(cmd(app, crate::refused_part::REPAIR, "Repair Part").with(json!({ "body": b })));
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
@@ -2595,6 +2595,32 @@ fn error_rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui: &mut
|
||||
crate::ui_kit::text(ui, &shown_error(&error), crate::ui_kit::Text::Hint("missing_pick"));
|
||||
return;
|
||||
}
|
||||
// An operation on a part the kernel refuses: the part, what is wrong with it, and what
|
||||
// to do, instead of the kernel's words and an issue report.
|
||||
if let Some(part) = refused_operand(app, d) {
|
||||
let operation =
|
||||
dialog_commands(app, d).ok().and_then(|c| c.iter().find_map(|(_, p)| p.get("operation").and_then(Value::as_str).map(str::to_string)));
|
||||
ui.vertical(|ui| {
|
||||
crate::ui_kit::text(ui, &crate::refused_part::message(&part, operation.as_deref()), crate::ui_kit::Text::Report("error", true));
|
||||
ui.horizontal(|ui| {
|
||||
let ready = solidcraft_engine::find_command(crate::refused_part::REPAIR).map(|c| c.info(&app.session));
|
||||
let enabled = ready.as_ref().is_some_and(|i| i.enabled);
|
||||
let repair = crate::ui_kit::button(ui, crate::i18n::tr("refused.repair"), crate::ui_kit::Button::Enabled(enabled));
|
||||
crate::scenario::publish_handle("dialog:repair_part", repair.rect.center());
|
||||
if repair.clicked() {
|
||||
crate::refused_part::repair(app, &part.body);
|
||||
}
|
||||
if part.face.is_some() {
|
||||
let show = crate::ui_kit::button(ui, crate::i18n::tr("refused.show_face"), crate::ui_kit::Button::Normal);
|
||||
crate::scenario::publish_handle("dialog:show_face", show.rect.center());
|
||||
if show.clicked() {
|
||||
crate::refused_part::show_face(app, &part);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
// A fillet's refusal reads in plain words, or not at all where its Setback field already
|
||||
// says what is wrong; the kernel's own text stays in the hover text, Copy Details and the
|
||||
// issue report.
|
||||
@@ -2669,6 +2695,13 @@ pub(crate) fn fillet_plain_error(error: &str, setback_in_field: bool) -> Option<
|
||||
}
|
||||
}
|
||||
|
||||
/// The part a failed dialog's operation uses that the kernel refused, if one did.
|
||||
fn refused_operand(app: &SolveApp, d: &Dialog) -> Option<crate::refused_part::RefusedPart> {
|
||||
let commands = dialog_commands(app, d).unwrap_or_default();
|
||||
let names = crate::refused_part::operands(app, &commands, &d.items());
|
||||
crate::refused_part::refused_among(&app.session.model.state(), names.iter().map(String::as_str))
|
||||
}
|
||||
|
||||
/// A refusal as the dialog shows it: without the engine's "invalid parameters for `id`: "
|
||||
/// prefix (the dialog already names the command), and in the interface language when the
|
||||
/// message is in the catalogs.
|
||||
|
||||
@@ -200,6 +200,7 @@ pub mod preview;
|
||||
mod primitive_units_tests;
|
||||
pub mod recovery_ui;
|
||||
pub mod ref_images;
|
||||
pub(crate) mod refused_part;
|
||||
pub mod render_workspace;
|
||||
#[cfg(test)]
|
||||
mod revolve_edit_tests;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//! A part the modelling kernel refuses: an operation on it fails, and the kernel (`Body::smlib_refusal`)
|
||||
//! says which face. Said in words where the operation fails (the feature dialog) and where the
|
||||
//! part is listed (the browser), with what to do: repair the part, or look at the face.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Sel;
|
||||
use solidcraft_engine::doc::kernel::SurfaceKind;
|
||||
use solidcraft_engine::doc::{FeatureKind, ModelState, PlaneRef};
|
||||
|
||||
use crate::SolveApp;
|
||||
|
||||
/// The command that repairs a part (planned).
|
||||
pub(crate) const REPAIR: &str = "body.repair";
|
||||
|
||||
/// A part an operation uses that the kernel refuses.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct RefusedPart {
|
||||
pub body: String,
|
||||
/// The face the kernel refuses (in the body's face order), when it found one.
|
||||
pub face: Option<usize>,
|
||||
pub surface: Option<SurfaceKind>,
|
||||
}
|
||||
|
||||
/// The first of `bodies` (names) the kernel refused when it was last sent there, in `st`.
|
||||
pub(crate) fn refused_among<'a>(st: &ModelState, bodies: impl IntoIterator<Item = &'a str>) -> Option<RefusedPart> {
|
||||
bodies.into_iter().find_map(|name| {
|
||||
let r = st.body(name)?.body.smlib_refused()?;
|
||||
Some(RefusedPart { body: name.to_string(), face: r.face, surface: r.surface })
|
||||
})
|
||||
}
|
||||
|
||||
/// The bodies a dialog's commands use: the ones their parameters name (targets, participants,
|
||||
/// a body), the ones its picks are on, and the body its sketch lies on.
|
||||
pub(crate) fn operands(app: &SolveApp, commands: &[(String, Value)], picks: &[Sel]) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
let mut add = |name: &str| {
|
||||
if !out.iter().any(|n| n == name) {
|
||||
out.push(name.to_string());
|
||||
}
|
||||
};
|
||||
for (_, p) in commands {
|
||||
for key in ["targets", "participants", "bodies"] {
|
||||
for name in p.get(key).and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str) {
|
||||
add(name);
|
||||
}
|
||||
}
|
||||
for key in ["body", "target"] {
|
||||
if let Some(name) = p.get(key).and_then(Value::as_str) {
|
||||
add(name);
|
||||
}
|
||||
}
|
||||
let sketch = p.get("sketch").and_then(Value::as_u64).and_then(|id| app.session.doc.feature(id));
|
||||
if let Some(FeatureKind::Sketch { plane: PlaneRef::Face { anchor: Some(anchor), .. }, .. }) = sketch.map(|f| &f.kind) {
|
||||
add(&anchor.target.body);
|
||||
}
|
||||
}
|
||||
for pick in picks {
|
||||
match pick {
|
||||
Sel::Body { name } | Sel::Face { body: name, .. } | Sel::Edge { body: name, .. } => add(name),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// What the dialog says: what could not be done to the part, then what is wrong with it.
|
||||
/// `operation` is the command's (`join`, `cut`, `intersect`; anything else is a change).
|
||||
pub(crate) fn message(part: &RefusedPart, operation: Option<&str>) -> String {
|
||||
let what = match operation {
|
||||
Some("join") => "refused.join",
|
||||
Some("cut") => "refused.cut",
|
||||
Some("intersect") => "refused.intersect",
|
||||
_ => "refused.change",
|
||||
};
|
||||
format!("{} {}", crate::i18n::format(what, &[("body", &part.body)]), crate::i18n::tr(why_key(part)))
|
||||
}
|
||||
|
||||
/// What is wrong with the part, as a sentence id.
|
||||
fn why_key(part: &RefusedPart) -> &'static str {
|
||||
match (part.face, part.surface) {
|
||||
(None, _) => "refused.faces_apart",
|
||||
(Some(_), Some(SurfaceKind::Plane)) => "refused.face_flat",
|
||||
(Some(_), Some(SurfaceKind::Cylinder | SurfaceKind::Cone)) => "refused.face_curved",
|
||||
(Some(_), Some(SurfaceKind::Sphere | SurfaceKind::Torus)) => "refused.face_rounded",
|
||||
(Some(_), Some(SurfaceKind::Freeform) | None) => "refused.face_freeform",
|
||||
}
|
||||
}
|
||||
|
||||
/// Repair the part (the command says when it cannot yet).
|
||||
pub(crate) fn repair(app: &mut SolveApp, body: &str) {
|
||||
if let Err(e) = app.run(REPAIR, json!({ "body": body })) {
|
||||
app.set_status(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the face the kernel refuses and frame it.
|
||||
pub(crate) fn show_face(app: &mut SolveApp, part: &RefusedPart) {
|
||||
let Some(face) = part.face else { return };
|
||||
let world = crate::open_meshing::session_world(&app.session);
|
||||
let Some(b) = world.body(&part.body) else { return };
|
||||
let mesh = b.mesh();
|
||||
let point = mesh.tri_face.iter().zip(&mesh.triangles).find(|(f, _)| **f as usize == face).and_then(|(_, t)| {
|
||||
let [a, b, c] = t.map(|i| mesh.positions.get(i as usize).copied());
|
||||
Some((a? + b? + c?) / 3.0)
|
||||
});
|
||||
let Some(point) = point else { return };
|
||||
let item = json!({"type": "face", "body": part.body, "index": face, "point": [point.x, point.y, point.z]});
|
||||
if app.run("select.set", json!({ "items": [item] })).is_ok() {
|
||||
let _ = app.run("view.fit", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The dialog names the part and what is wrong in a CAD user's words: no kernel name, no
|
||||
/// status code.
|
||||
#[test]
|
||||
fn a_refused_part_is_named_with_what_is_wrong_with_it() {
|
||||
let _english = crate::i18n::scope("en");
|
||||
let rounded = RefusedPart { body: "VALVE_COVER_01".into(), face: Some(412), surface: Some(SurfaceKind::Torus) };
|
||||
assert_eq!(message(&rounded, Some("join")), "Couldn't join to VALVE_COVER_01. One of its rounded faces can't be used for modelling.");
|
||||
let apart = RefusedPart { body: "Bracket".into(), face: None, surface: None };
|
||||
assert_eq!(message(&apart, Some("cut")), "Couldn't cut Bracket. Its faces don't meet closely enough to be used for modelling.");
|
||||
let flat = RefusedPart { body: "Plate".into(), face: Some(0), surface: Some(SurfaceKind::Plane) };
|
||||
let text = message(&flat, None);
|
||||
assert_eq!(text, "Couldn't change Plate. One of its flat faces can't be used for modelling.");
|
||||
assert!(!text.contains("SMLib") && !text.contains("SM_ERR"), "{text}");
|
||||
}
|
||||
}
|
||||
@@ -403,6 +403,7 @@ pub(crate) fn import_summary(report: &ImportReport) -> String {
|
||||
("import_report.left_out", faces_of(IssueKind::FaceLeftOut)),
|
||||
("import_report.open", parts_of(IssueKind::OpenBody)),
|
||||
("import_report.not_shown", faces_of(IssueKind::FacesNotShown)),
|
||||
("import_report.needs_repair", parts_of(IssueKind::NeedsRepair)),
|
||||
("import_report.repaired", parts_of(IssueKind::Repaired)),
|
||||
("import_report.file", file),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user