use anyhow::Result;
use kittycad_modeling_cmds::units::UnitLength;
use crate::SourceRange;
use crate::engine::DEFAULT_PLANE_INFO;
use crate::engine::PlaneName;
use crate::errors::Suggestion;
use crate::execution::PlaneInfo;
use crate::execution::Point3d;
use crate::lint::rule::Discovered;
use crate::lint::rule::Finding;
use crate::lint::rule::def_finding;
use crate::parsing::ast::types::BinaryPart;
use crate::parsing::ast::types::CallExpressionKw;
use crate::parsing::ast::types::Expr;
use crate::parsing::ast::types::LiteralValue;
use crate::parsing::ast::types::Node as AstNode;
use crate::parsing::ast::types::ObjectExpression;
use crate::parsing::ast::types::Program;
use crate::parsing::ast::types::UnaryOperator;
use crate::walk::Node;
def_finding!(
Z0003,
"offsetPlane should be used to define a new plane offset from the origin",
"\
startSketchOn should be an offsetPlane call in this case ✏️
The startSketchOn stdlib function has the ability to define a custom Plane
to begin the sketch on (outside of the built in XY, -YZ planes). There also
exists the offsetPlane stdlib function to create a new Plane offset by some
fixed amount from an existing plane.
This lint rule triggers when a startSketchOn's provided plane is recognized as
being merely offset from a built-in plane. It's much more readable to
use offsetPlane where possible.
",
crate::lint::rule::FindingFamily::Simplify
);
pub fn lint_should_be_offset_plane(node: Node, _prog: &AstNode<Program>) -> Result<Vec<Discovered>> {
let Some((call_source_range, plane_name, offset)) = start_sketch_on_check_specific_plane(node)? else {
return Ok(vec![]);
};
if offset == 0.0 {
return Ok(vec![]);
}
let suggestion = Suggestion {
title: "use offsetPlane instead".to_owned(),
insert: format!("offsetPlane({plane_name}, offset = {offset})"),
source_range: call_source_range,
};
Ok(vec![Z0003.at(
format!("custom plane in startSketchOn; offsetPlane from {plane_name} would work here"),
call_source_range,
Some(suggestion),
)])
}
fn get_xyz(point: &ObjectExpression) -> Option<(f64, f64, f64)> {
let mut x: Option<f64> = None;
let mut y: Option<f64> = None;
let mut z: Option<f64> = None;
fn unlitafy(lit: &LiteralValue) -> Option<f64> {
Some(match lit {
LiteralValue::Number { value, .. } => *value,
_ => {
return None;
}
})
}
for property in &point.properties {
let Some(value) = (match &property.value {
Expr::UnaryExpression(value) => {
if value.operator != UnaryOperator::Neg {
continue;
}
let BinaryPart::Literal(value) = &value.inner.argument else {
continue;
};
unlitafy(&value.inner.value).map(|v| -v)
}
Expr::Literal(value) => unlitafy(&value.value),
_ => {
continue;
}
}) else {
continue;
};
match property.key.inner.name.as_str() {
"x" => x = Some(value),
"y" => y = Some(value),
"z" => z = Some(value),
_ => {}
}
}
Some((x?, y?, z?))
}
fn get_offset(info: &PlaneInfo) -> Option<f64> {
if info.origin.x != 1.0 && info.origin.x != -1.0 && info.origin.x != 0.0 {
return Some(info.origin.x);
} else if info.origin.y != 1.0 && info.origin.y != -1.0 && info.origin.y != 0.0 {
return Some(info.origin.y);
} else if info.origin.z != 1.0 && info.origin.z != -1.0 && info.origin.z != 0.0 {
return Some(info.origin.z);
} else if info.x_axis.x != 1.0 && info.x_axis.x != -1.0 && info.x_axis.x != 0.0 {
return Some(info.x_axis.x);
} else if info.x_axis.y != 1.0 && info.x_axis.y != -1.0 && info.x_axis.y != 0.0 {
return Some(info.x_axis.y);
} else if info.x_axis.z != 1.0 && info.x_axis.z != -1.0 && info.x_axis.z != 0.0 {
return Some(info.x_axis.z);
} else if info.y_axis.x != 1.0 && info.y_axis.x != -1.0 && info.y_axis.x != 0.0 {
return Some(info.y_axis.x);
} else if info.y_axis.y != 1.0 && info.y_axis.y != -1.0 && info.y_axis.y != 0.0 {
return Some(info.y_axis.y);
} else if info.y_axis.z != 1.0 && info.y_axis.z != -1.0 && info.y_axis.z != 0.0 {
return Some(info.y_axis.z);
}
None
}
pub fn start_sketch_on_check_specific_plane(node: Node) -> Result<Option<(SourceRange, PlaneName, f64)>> {
match node {
Node::CallExpressionKw(node) => start_sketch_on_check_specific_plane_kw(node),
_ => Ok(None),
}
}
pub fn start_sketch_on_check_specific_plane_kw(
call: &AstNode<CallExpressionKw>,
) -> Result<Option<(SourceRange, PlaneName, f64)>> {
if call.inner.callee.inner.name.name != "startSketchOn" {
return Ok(None);
}
let Some(ref unlabeled) = call.inner.unlabeled else {
return Ok(None);
};
let call_source_range = SourceRange::new(unlabeled.start(), unlabeled.end(), unlabeled.module_id());
let Expr::ObjectExpression(arg) = &unlabeled else {
return Ok(None);
};
common(arg, call_source_range)
}
pub fn common(
arg: &AstNode<ObjectExpression>,
call_source_range: SourceRange,
) -> Result<Option<(SourceRange, PlaneName, f64)>> {
let mut origin: Option<Point3d> = None;
let mut x_vec: Option<Point3d> = None;
let mut y_vec: Option<Point3d> = None;
for property in &arg.inner.properties {
let Expr::ObjectExpression(ref point) = property.inner.value else {
return Ok(None);
};
let Some((x, y, z)) = get_xyz(&point.inner) else {
return Ok(None);
};
let property_name = &property.inner.key.inner.name;
match property_name.as_str() {
"origin" => {
origin = Some(Point3d {
x,
y,
z,
units: Some(UnitLength::Millimeters),
})
}
"xAxis" => x_vec = Some(Point3d { x, y, z, units: None }),
"yAxis" => y_vec = Some(Point3d { x, y, z, units: None }),
_ => {
continue;
}
};
}
let (Some(origin), Some(x_vec), Some(y_vec)) = (origin, x_vec, y_vec) else {
return Ok(None);
};
if [origin.x, origin.y, origin.z].iter().filter(|v| **v == 0.0).count() < 2 {
return Ok(None);
}
let plane_info = PlaneInfo {
origin,
x_axis: x_vec,
y_axis: y_vec,
z_axis: x_vec.axes_cross_product(&y_vec),
};
let plane_equal_excluding_z = |plane: &&PlaneInfo, plane_info: &PlaneInfo| {
plane.origin == plane_info.origin && plane.x_axis == plane_info.x_axis && plane.y_axis == plane_info.y_axis
};
if let Some((name, _)) = DEFAULT_PLANE_INFO
.iter()
.find(|(_, plane)| plane_equal_excluding_z(plane, &plane_info))
{
return Ok(Some((call_source_range, *name, 0.0)));
}
let normalized_plane_info = normalize_plane_info(&plane_info);
println!("normalized plane info: {:#?}", normalized_plane_info);
let Some((matched_plane_name, _)) = DEFAULT_PLANE_INFO
.iter()
.find(|(_, plane)| plane_equal_excluding_z(plane, &normalized_plane_info))
else {
return Ok(None);
};
let Some(offset) = get_offset(&plane_info) else {
return Ok(None);
};
Ok(Some((call_source_range, *matched_plane_name, offset)))
}
fn normalize_plane_info(plane_info: &PlaneInfo) -> PlaneInfo {
let mut normalized_plane_info = plane_info.clone();
normalized_plane_info.origin = Point3d {
x: 0.0,
y: 0.0,
z: 0.0,
units: normalized_plane_info.origin.units,
};
normalized_plane_info.y_axis.x = if normalized_plane_info.y_axis.x != 0.0 {
normalized_plane_info.y_axis.x.signum()
} else {
0.0
};
normalized_plane_info.y_axis.y = if normalized_plane_info.y_axis.y != 0.0 {
normalized_plane_info.y_axis.y.signum()
} else {
0.0
};
normalized_plane_info.y_axis.z = if normalized_plane_info.y_axis.z != 0.0 {
normalized_plane_info.y_axis.z.signum()
} else {
0.0
};
normalized_plane_info
}
#[cfg(test)]
mod tests {
use super::Z0003;
use super::lint_should_be_offset_plane;
use crate::lint::rule::test_finding;
use crate::lint::rule::test_no_finding;
test_finding!(
z0003_bad_sketch_on,
lint_should_be_offset_plane,
Z0003,
"\
startSketchOn({
origin = { x = 0, y = -14.3, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
})
|> startProfile(at = [0, 0])
",
"custom plane in startSketchOn; offsetPlane from XZ would work here",
Some("offsetPlane(XZ, offset = -14.3)".to_string())
);
test_finding!(
z0003_bad_sketch_on_not_normalized_axes,
lint_should_be_offset_plane,
Z0003,
"\
a1 = startSketchOn({
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 12, z = 0 },
})
|> startProfile(at = [0, 0])
|> line(end = [100.0, 0])
|> yLine(length = -100.0)
|> xLine(length = -100.0)
|> yLine(length = 100.0)
|> close()
|> extrude(length = 3.14)
",
"custom plane in startSketchOn; offsetPlane from XY would work here",
Some("offsetPlane(XY, offset = 12)".to_string())
);
test_no_finding!(
z0003_good_sketch_on,
lint_should_be_offset_plane,
Z0003,
"\
startSketchOn({
origin = { x = 10, y = -14.3, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
})
"
);
test_no_finding!(
z0003_default_plane,
lint_should_be_offset_plane,
Z0003,
"\
startSketchOn({
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 0, z = 1 },
})
"
);
}