Repair Part: the repair runs once, as the feature evaluates

The command worked the repair out itself for its reply and the feature worked it out again
when it evaluated. The evaluation now keeps the kernel's report on the feature's result (as
an import keeps its import report, cached with it), and the command's reply is built from
that: one repair per call, and a part with nothing to repair fails the evaluation, which
leaves the design unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 23:52:16 -04:00
committed by Irakli Kourtis
parent f56e34d1ce
commit f351f42df6
3 changed files with 30 additions and 13 deletions
+13 -1
View File
@@ -734,6 +734,7 @@ struct CachedEval {
error: Option<String>,
warning: Option<String>,
import_report: Option<Arc<kernel::ImportReport>>,
repair: Option<Arc<kernel::RepairReport>>,
ms: f64,
state: Arc<ModelState>,
/// What the state costs, worked out once when it was stored (see
@@ -943,6 +944,9 @@ pub struct FeatureResult {
/// (`warning` is the first of them as the kernel words them).
#[serde(skip_serializing_if = "Option::is_none")]
pub import_report: Option<Arc<kernel::ImportReport>>,
/// What a Repair Part feature changed, face by face (the command's reply says it).
#[serde(skip)]
pub repair: Option<Arc<kernel::RepairReport>>,
pub ms: f64,
pub skipped: bool,
/// Not evaluated on the document's current inputs: a feature at or after a sketch shown live
@@ -1874,6 +1878,7 @@ impl Model {
error: None,
warning: None,
import_report: None,
repair: None,
ms: 0.0,
skipped: true,
stale: false,
@@ -1910,6 +1915,7 @@ impl Model {
error: c.error,
warning: c.warning,
import_report: c.import_report,
repair: c.repair,
ms: c.ms,
skipped: false,
stale: false,
@@ -1944,6 +1950,7 @@ impl Model {
}
let mut warning = None;
let mut import_report = None;
let mut repair = None;
// Face references by name: move each face point onto its named face (on the body the
// feature names, when it names one and the face is there).
let renamed;
@@ -1967,7 +1974,8 @@ impl Model {
let r = match frame_error.or(bad_param) {
Some(e) => Err(kernel::KernelError::Failed(e)),
None => kernel::guard(&f.name, || {
eval_feature(doc, &vals, f, &mut next, &mut warning, &mut import_report).map_err(|e| kernel::KernelError::Failed(e.to_string()))
eval_feature(doc, &vals, f, &mut next, &mut warning, &mut import_report, &mut repair)
.map_err(|e| kernel::KernelError::Failed(e.to_string()))
}),
};
if r.is_ok() {
@@ -2023,6 +2031,7 @@ impl Model {
error: error.clone(),
warning: warning.clone(),
import_report: import_report.clone(),
repair: repair.clone(),
ms,
bytes: state.added_bytes(&before),
state: state.clone(),
@@ -2041,6 +2050,7 @@ impl Model {
error,
warning,
import_report,
repair,
ms,
skipped: false,
stale: false,
@@ -6237,6 +6247,7 @@ fn eval_feature(
st: &mut ModelState,
warning: &mut Option<String>,
import_report: &mut Option<Arc<kernel::ImportReport>>,
repair: &mut Option<Arc<kernel::RepairReport>>,
) -> Result<()> {
// Its tools meet bodies in other placements where those are shown (#3383).
let _frames = ToolFrames::install(doc, f, st);
@@ -7040,6 +7051,7 @@ fn eval_feature(
}
let target = st.bodies.get_mut(index).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
*target = result;
*repair = Some(Arc::new(report));
Ok(())
}
FeatureKind::MeshImport { meshes, .. } => {
+2
View File
@@ -596,6 +596,7 @@ impl Model {
.get("import_report")
.map(|v| serde_json::from_value::<kernel::ImportReport>(v.clone()).map(Arc::new).map_err(|_| bad("saved model: import report")))
.transpose()?,
repair: None,
ms,
skipped: r.get("skipped").and_then(Json::as_bool).ok_or_else(|| bad("saved model: skipped"))?,
stale: false,
@@ -634,6 +635,7 @@ impl Model {
error: None,
warning: r.warning.clone(),
import_report: r.import_report.clone(),
repair: r.repair.clone(),
ms: r.ms,
bytes: r.state.retained_bytes(),
state: r.state.clone(),
+15 -12
View File
@@ -815,12 +815,12 @@ pub(crate) fn counted(f: &solidcraft_doc::Feature, i: usize) -> bool {
f.body_names.get(i).is_none_or(|n| n.trim().is_empty()) && (f.shell_piece_order || i < f.body_names.len() || !first)
}
/// Repair Part (#8371): the part's repair is worked out first (refused with why when nothing
/// applies), then made a feature, which works it out again as it evaluates; the reply says what
/// changed, face by face.
/// Repair Part (#8371): a feature, which repairs the part as it evaluates (a part with nothing
/// to repair fails it, with why, and the command changes nothing); the reply is what that
/// evaluation changed, face by face.
fn repair_body(s: &mut Session, p: &Value) -> Result<Value> {
use solidcraft_doc::expr::Kind;
use solidcraft_kernel::{Continuity, RepairOptions, SurfaceKind};
use solidcraft_kernel::{Continuity, SurfaceKind};
let cmd = "body.repair";
let name = str_(p, "body").filter(|b| !b.trim().is_empty()).ok_or_else(|| bad(cmd, "`body` must name the part to repair"))?.to_string();
let state = s.model.state();
@@ -830,11 +830,17 @@ fn repair_body(s: &mut Session, p: &Value) -> Result<Value> {
}
let gap = expr(p, "gap").unwrap_or_else(|| "0.2 mm".into());
check_expr(s, &gap, Kind::Length, cmd, "gap")?;
let gap_mm = s.doc.eval(&gap, Kind::Length).map_err(|e| bad(cmd, format!("gap: {e}")))?;
let patch_holes = bool_(p, "patch_holes").unwrap_or(false);
let (repaired, report) =
solidcraft_kernel::repair(&source.body, &RepairOptions { gap: gap_mm, patch_holes }).map_err(|e| bad(cmd, e.to_string()))?;
let volume = if report.closed { solidcraft_kernel::measure(&repaired).ok().map(|m| m.volume) } else { None };
let v = add_feature(s, p, FeatureKind::Repair { body: name.clone().into(), gap, patch_holes })?;
let field = |k: &str| v.get(k).cloned().unwrap_or(Value::Null);
let feature = field("feature").as_u64().unwrap_or(0);
let report = s
.model
.result(feature)
.and_then(|r| r.repair.clone())
.ok_or_else(|| crate::EngineError::Other(format!("{cmd}: the feature kept no report of the repair")))?;
let repaired = s.model.state().body(&name).map(|b| b.body.clone());
let volume = repaired.filter(|_| report.closed).and_then(|b| solidcraft_kernel::measure(&b).ok()).map(|m| m.volume);
let surface = |k: SurfaceKind| format!("{k:?}").to_lowercase();
let face = |face: usize, k: SurfaceKind, why: &str| RepairFace { face, surface: surface(k), why: why.to_string() };
let done = RepairDone {
@@ -862,13 +868,10 @@ fn repair_body(s: &mut Session, p: &Value) -> Result<Value> {
modelling: report.modelling,
volume,
};
let kind = FeatureKind::Repair { body: name.clone().into(), gap, patch_holes };
let v = add_feature(s, p, kind)?;
let field = |k: &str| v.get(k).cloned().unwrap_or(Value::Null);
let list =
|k: &str| v.get(k).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default();
let reply = RepairResult {
feature: field("feature").as_u64().unwrap_or(0),
feature,
name: field("name").as_str().unwrap_or_default().to_string(),
bodies: list("bodies"),
created_bodies: list("created_bodies"),