///|
/// Severity classifies a `Diagnostic` produced by `Design::diagnostics`.
pub enum Severity {
Info
Warning
Error
}
///|
/// Diagnostic describes something surprising about a `Design` without
/// compiling it.
///
/// The STEP backend never trims or subtracts: a top-level `Union` or
/// `Difference` concatenates its children's faces. Those nodes are reported as
/// nominal (a warning). A boolean nested inside another boolean contributes no
/// faces at all and is reported as an error.
pub enum Diagnostic {
/// A top-level `Union` whose faces are concatenated, not merged.
NominalUnion(String)
/// A top-level `Difference` whose faces are concatenated, not subtracted.
NominalDifference(String)
/// A boolean nested inside another boolean; it contributes no faces.
DroppedNestedBoolean(String)
/// An edge fillet/chamfer that the kernel-free flat writer cannot evaluate.
EdgeFeatureRequiresKernel(String)
/// A shape whose compiler produces no faces (empty or degenerate).
EmptySolid(String)
}
///|
/// Severity::is_error reports whether this severity blocks checked export.
pub fn Severity::is_error(self : Severity) -> Bool {
match self {
Error => true
_ => false
}
}
///|
/// Severity::all lists the severity levels from least to most urgent.
pub fn Severity::all() -> Array[Severity] {
[Info, Warning, Error]
}
///|
/// Diagnostic::is_nominal reports whether the diagnostic describes a nominal
/// (face-concatenating) boolean.
pub fn Diagnostic::is_nominal(self : Diagnostic) -> Bool {
match self {
NominalUnion(_) | NominalDifference(_) => true
_ => false
}
}
///|
pub fn Diagnostic::severity(self : Diagnostic) -> Severity {
match self {
NominalUnion(_) => Warning
NominalDifference(_) => Warning
DroppedNestedBoolean(_) => Error
EdgeFeatureRequiresKernel(_) => Error
EmptySolid(_) => Error
}
}
///|
pub fn Diagnostic::message(self : Diagnostic) -> String {
match self {
NominalUnion(name) =>
"union '\{name}' is nominal: faces are concatenated, not merged. Use an ExtrudedProfile with inner loops, or export via the Blender backend."
NominalDifference(name) =>
"difference '\{name}' is nominal: faces are concatenated, not subtracted. Use an ExtrudedProfile with inner loops, or export via the Blender backend."
DroppedNestedBoolean(name) =>
"boolean '\{name}' is nested inside another boolean and contributes no faces. Flatten the design or split it into separate nodes."
EdgeFeatureRequiresKernel(name) =>
"edge feature '\{name}' requires an evaluated kernel: the flat STEP writer cannot trim edges. Export with Design::to_step_evaluated() (OCCT) or the Blender backend."
EmptySolid(name) =>
"shape '\{name}' produced no faces. Check its dimensions, profile, or path."
}
}
///|
/// collect_diagnostics walks a shape tree, reporting nominal booleans at the
/// top level and dropped booleans deeper down.
fn collect_diagnostics(
shape : Shape,
name : String,
depth : Int32,
out : Array[Diagnostic],
) -> Unit {
match shape {
Union(u) => {
if depth == 0 {
out.push(NominalUnion(name))
} else {
out.push(DroppedNestedBoolean(name))
}
if u.nodes.is_empty() {
out.push(EmptySolid(name))
}
for child in u.nodes {
collect_diagnostics(child.shape, child.name, depth + 1, out)
}
}
Difference(d) => {
if depth == 0 {
out.push(NominalDifference(name))
} else {
out.push(DroppedNestedBoolean(name))
}
collect_diagnostics(d.base.shape, d.base.name, depth + 1, out)
for cutter in d.cutters {
collect_diagnostics(cutter.shape, cutter.name, depth + 1, out)
}
}
Fillet(f) => {
out.push(EdgeFeatureRequiresKernel(name))
collect_diagnostics(f.base.shape, f.base.name, depth + 1, out)
}
Chamfer(c) => {
out.push(EdgeFeatureRequiresKernel(name))
collect_diagnostics(c.base.shape, c.base.name, depth + 1, out)
}
Cylinder(c) =>
if c.diameter <= 0.0 || c.height <= 0.0 {
out.push(EmptySolid(name))
}
ExtrudedProfile(e) =>
if e.height_mm == 0.0 || e.profile.outer.length() < 3 {
out.push(EmptySolid(name))
}
ExtrudedBezierProfile(e) =>
if e.height_mm == 0.0 {
out.push(EmptySolid(name))
}
SweptProfile(s) =>
if s.path.points.length() < 2 || s.profile.vertices.length() < 3 {
out.push(EmptySolid(name))
}
_ => ()
}
}
///|
/// Design::diagnostics reports nominal and dropped booleans plus shapes that
/// would compile to nothing. It is a pure observation and does not compile or
/// mutate the design.
pub fn Design::diagnostics(self : Design) -> Array[Diagnostic] {
let out : Array[Diagnostic] = []
for node in self.nodes {
collect_diagnostics(node.shape, node.name, 0, out)
}
out
}