#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FaceTransformRefusal {
pub motion: &'static str,
pub reason: String,
pub hint: String,
pub kernel: String,
}
impl FaceTransformRefusal {
pub fn one_line(&self) -> String {
format!("{} — {}", self.reason, self.hint)
}
}
fn subject(faces: &[String]) -> String {
match faces {
[] => "the selected face".to_string(),
[one] => format!("`{one}`"),
many => format!("the {} selected faces", many.len()),
}
}
fn neighbours(message: &str, faces: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for (index, span) in message.split('`').enumerate() {
if index % 2 == 1
&& !span.is_empty()
&& !faces.iter().any(|f| f == span)
&& !out.iter().any(|n| n == span)
{
out.push(span.to_string());
}
}
out
}
fn and_list(names: &[String]) -> String {
match names {
[] => String::new(),
[one] => format!("`{one}`"),
[rest @ .., last] => format!(
"{} and `{last}`",
rest.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ")
),
}
}
pub fn selected_face_names(faces: Option<&serde_json::Value>) -> Vec<String> {
brep_kernel::reference_names(faces)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RefusalFacts {
pub reason: Option<brep_kernel::FaceTransformReason>,
pub turning: bool,
}
impl RefusalFacts {
pub fn from_report(report: &serde_json::Value, id: &str) -> Self {
let Some(entry) = report.get("featureRefusals").and_then(|map| map.get(id)) else {
return Self::default();
};
let turning = entry.get("step").and_then(serde_json::Value::as_str) == Some("rotation");
let mut refusal = entry.clone();
if let serde_json::Value::Object(object) = &mut refusal {
object.remove("step");
}
let reason = serde_json::from_value::<brep_kernel::KernelRefusal>(refusal)
.ok()
.and_then(|refusal| brep_kernel::face_transform_reason(&refusal));
Self { reason, turning }
}
}
pub fn explain_refusal(message: &str, facts: RefusalFacts, faces: &[String]) -> FaceTransformRefusal {
use brep_kernel::FaceTransformReason as Reason;
let kernel = message.to_string();
let subject = subject(faces);
let motion = if facts.turning { "Rotate Face" } else { "Move Face" };
let turning = motion == "Rotate Face";
let motion_noun = if turning { "turn" } else { "move" };
let blocked = neighbours(message, faces);
let by_neighbour = if blocked.is_empty() {
String::new()
} else {
format!(" {} could not follow it", and_list(&blocked))
};
let (reason, hint) = if facts.reason == Some(Reason::InsideOut) {
(
format!(
"That {motion_noun} carries {subject} clean through the far side of the body, \
leaving it inside out."
),
"Ease it back until the model follows again — while it refuses the feature builds \
nothing at all, so the face springs back to where it started."
.to_string(),
)
} else if facts.reason == Some(Reason::PastNeighbour) {
(
format!(
"That {motion_noun} carries {subject} past a face it has to meet, so{} \
without turning a corner of the body inside out.",
if by_neighbour.is_empty() {
" its neighbour could not follow it".to_string()
} else {
by_neighbour
}
),
if turning {
"Ease the angle back, or move the Pivot — the turn happens about it, so a pivot \
nearer the blocked corner asks less of it."
.to_string()
} else {
"Ease the distance back until the model follows again, or select the blocked \
neighbours too so they move with it."
.to_string()
},
)
} else if facts.reason == Some(Reason::TurnParallel) {
(
format!(
"That turn lays {subject} parallel to a face it has to meet, and two parallel \
faces never make a corner."
),
"Stop short of the angle that flattens it — while it refuses the feature builds \
nothing at all, so the face springs back unturned."
.to_string(),
)
} else if facts.reason == Some(Reason::RigidScale) {
(
format!("A face transform carries {subject} rigidly, so it cannot scale."),
"Clear the scale back to 1, 1, 1 and move or turn the face instead.".to_string(),
)
} else if facts.reason == Some(Reason::NoPivot) {
(
format!("{subject} has no geometry to turn about."),
"Pick the face again, or type a Pivot to turn about.".to_string(),
)
} else {
(
kernel.clone(),
"Ease the value back until the model follows again — while it refuses the feature \
builds nothing at all, so the face springs back to where it started."
.to_string(),
)
};
FaceTransformRefusal {
motion,
reason,
hint,
kernel,
}
}