#[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)
}
pub fn explain_refusal(message: &str, faces: &[String]) -> FaceTransformRefusal {
let kernel = message.to_string();
let lower = message.to_ascii_lowercase();
let subject = subject(faces);
let motion = if lower.contains("rotation step") {
"Rotate Face"
} else if lower.contains("translation step") {
"Move Face"
} else if lower.contains("rotate_faces") || lower.contains("the rotation") {
"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 lower.contains("signed volume changed sign")
|| lower.contains("inverts the solid")
{
(
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 lower.contains("inverts edge") || lower.contains("passes beyond its neighbour") {
(
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 lower.contains("parallel") {
(
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 lower.contains("transform.scale") {
(
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 lower.contains("centre a pivot on") || lower.contains("center a pivot on") {
(
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,
}
}