Skip to content

Commit 72b6930

Browse files
authored
Sketch on normal of face (#7886)
This work updates `startSketchOn` to enable sketching on a plane normal to the selected face. You can now specify an axis of the face that will become the x axis of the new plane, with the normal of the selected face becoming the y axis of the new plane. This extension creates the plane and begins sketching as usual.
1 parent adde4c0 commit 72b6930

File tree

13 files changed

+318
-18
lines changed

13 files changed

+318
-18
lines changed

docs/kcl-std/functions/std-sketch-startSketchOn.md

Lines changed: 28 additions & 1 deletion
Large diffs are not rendered by default.

rust/kcl-derive-docs/src/example_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ pub const TEST_NAMES: &[&str] = &[
170170
"std-sketch-startSketchOn-3",
171171
"std-sketch-startSketchOn-4",
172172
"std-sketch-startSketchOn-5",
173+
"std-sketch-startSketchOn-6",
173174
"std-sketch-angledLineThatIntersects-0",
174175
"std-sketch-angledLine-0",
175176
"std-sketch-arc-0",

rust/kcl-lib/src/execution/geometry.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::ops::{Add, AddAssign, Mul};
1+
use std::ops::{Add, AddAssign, Mul, Sub, SubAssign};
22

33
use anyhow::Result;
44
use indexmap::IndexMap;
@@ -561,6 +561,15 @@ impl Plane {
561561
pub fn is_standard(&self) -> bool {
562562
!matches!(self.value, PlaneType::Custom | PlaneType::Uninit)
563563
}
564+
565+
/// Project a point onto a plane by calculating how far away it is and moving it along the
566+
/// normal of the plane so that it now lies on the plane.
567+
pub fn project(&self, point: Point3d) -> Point3d {
568+
let v = point - self.info.origin;
569+
let dot = v.axes_dot_product(&self.info.z_axis);
570+
571+
point - self.info.z_axis * dot
572+
}
564573
}
565574

566575
/// A face.
@@ -1062,6 +1071,34 @@ impl AddAssign for Point3d {
10621071
}
10631072
}
10641073

1074+
impl Sub for Point3d {
1075+
type Output = Point3d;
1076+
1077+
fn sub(self, rhs: Self) -> Self::Output {
1078+
let (x, y, z) = if rhs.units != self.units {
1079+
(
1080+
rhs.units.adjust_to(rhs.x, self.units).0,
1081+
rhs.units.adjust_to(rhs.y, self.units).0,
1082+
rhs.units.adjust_to(rhs.z, self.units).0,
1083+
)
1084+
} else {
1085+
(rhs.x, rhs.y, rhs.z)
1086+
};
1087+
Point3d {
1088+
x: self.x - x,
1089+
y: self.y - y,
1090+
z: self.z - z,
1091+
units: self.units,
1092+
}
1093+
}
1094+
}
1095+
1096+
impl SubAssign for Point3d {
1097+
fn sub_assign(&mut self, rhs: Self) {
1098+
*self = *self - rhs
1099+
}
1100+
}
1101+
10651102
impl Mul<f64> for Point3d {
10661103
type Output = Point3d;
10671104

rust/kcl-lib/src/execution/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,49 @@ notPipeSub = 1 |> identity(!%))";
21672167
// let notInfinity = !Infinity
21682168
}
21692169

2170+
#[tokio::test(flavor = "multi_thread")]
2171+
async fn test_start_sketch_on_invalid_kwargs() {
2172+
let current_dir = std::env::current_dir().unwrap();
2173+
let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2174+
let mut code = std::fs::read_to_string(&path).unwrap();
2175+
assert_eq!(
2176+
parse_execute(&code).await.unwrap_err().message(),
2177+
"You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2178+
);
2179+
2180+
path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2181+
code = std::fs::read_to_string(&path).unwrap();
2182+
2183+
assert_eq!(
2184+
parse_execute(&code).await.unwrap_err().message(),
2185+
"`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2186+
);
2187+
2188+
path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2189+
code = std::fs::read_to_string(&path).unwrap();
2190+
2191+
assert_eq!(
2192+
parse_execute(&code).await.unwrap_err().message(),
2193+
"`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2194+
);
2195+
2196+
path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2197+
code = std::fs::read_to_string(&path).unwrap();
2198+
2199+
assert_eq!(
2200+
parse_execute(&code).await.unwrap_err().message(),
2201+
"`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2202+
);
2203+
2204+
path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2205+
code = std::fs::read_to_string(&path).unwrap();
2206+
2207+
assert_eq!(
2208+
parse_execute(&code).await.unwrap_err().message(),
2209+
"`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2210+
);
2211+
}
2212+
21702213
#[tokio::test(flavor = "multi_thread")]
21712214
async fn test_math_negative_variable_in_binary_expression() {
21722215
let ast = r#"sigmaAllow = 35000 // psi

rust/kcl-lib/src/std/planes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub async fn plane_of(exec_state: &mut ExecState, args: Args) -> Result<KclValue
2525
.map(|value| KclValue::Plane { value })
2626
}
2727

28-
async fn inner_plane_of(
28+
pub(crate) async fn inner_plane_of(
2929
solid: crate::execution::Solid,
3030
face: FaceTag,
3131
exec_state: &mut ExecState,

rust/kcl-lib/src/std/sketch.rs

Lines changed: 128 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Functions related to sketching.
22
3+
use std::f64;
4+
35
use anyhow::Result;
46
use indexmap::IndexMap;
57
use kcmc::shared::Point2d as KPoint2d; // Point2d is already defined in this pkg, to impl ts_rs traits.
@@ -20,13 +22,15 @@ use crate::execution::{Artifact, ArtifactId, CodeRef, StartSketchOnFace, StartSk
2022
use crate::{
2123
errors::{KclError, KclErrorDetails},
2224
execution::{
23-
BasePath, ExecState, Face, GeoMeta, KclValue, ModelingCmdMeta, Path, Plane, PlaneInfo, Point2d, Sketch,
24-
SketchSurface, Solid, TagEngineInfo, TagIdentifier,
25+
BasePath, ExecState, Face, GeoMeta, KclValue, ModelingCmdMeta, Path, Plane, PlaneInfo, Point2d, Point3d,
26+
Sketch, SketchSurface, Solid, TagEngineInfo, TagIdentifier,
2527
types::{ArrayLen, NumericType, PrimitiveType, RuntimeType, UnitLen},
2628
},
2729
parsing::ast::types::TagNode,
2830
std::{
2931
args::{Args, TyF64},
32+
axis_or_reference::Axis2dOrEdgeReference,
33+
planes::inner_plane_of,
3034
utils::{
3135
TangentialArcInfoInput, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
3236
intersection_with_parallel_line, point_to_len_unit, point_to_mm, untyped_point_to_mm,
@@ -810,8 +814,11 @@ pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<K
810814
exec_state,
811815
)?;
812816
let face = args.get_kw_arg_opt("face", &RuntimeType::tagged_face(), exec_state)?;
817+
let normal_to_face = args.get_kw_arg_opt("normalToFace", &RuntimeType::tagged_face(), exec_state)?;
818+
let align_axis = args.get_kw_arg_opt("alignAxis", &RuntimeType::Primitive(PrimitiveType::Axis2d), exec_state)?;
819+
let normal_offset = args.get_kw_arg_opt("normalOffset", &RuntimeType::length(), exec_state)?;
813820

814-
match inner_start_sketch_on(data, face, exec_state, &args).await? {
821+
match inner_start_sketch_on(data, face, normal_to_face, align_axis, normal_offset, exec_state, &args).await? {
815822
SketchSurface::Plane(value) => Ok(KclValue::Plane { value }),
816823
SketchSurface::Face(value) => Ok(KclValue::Face { value }),
817824
}
@@ -820,9 +827,43 @@ pub async fn start_sketch_on(exec_state: &mut ExecState, args: Args) -> Result<K
820827
async fn inner_start_sketch_on(
821828
plane_or_solid: SketchData,
822829
face: Option<FaceTag>,
830+
normal_to_face: Option<FaceTag>,
831+
align_axis: Option<Axis2dOrEdgeReference>,
832+
normal_offset: Option<TyF64>,
823833
exec_state: &mut ExecState,
824834
args: &Args,
825835
) -> Result<SketchSurface, KclError> {
836+
let face = match (face, normal_to_face, &align_axis, &normal_offset) {
837+
(Some(_), Some(_), _, _) => {
838+
return Err(KclError::new_semantic(KclErrorDetails::new(
839+
"You cannot give both `face` and `normalToFace` params, you have to choose one or the other."
840+
.to_owned(),
841+
vec![args.source_range],
842+
)));
843+
}
844+
(Some(face), None, None, None) => Some(face),
845+
(_, Some(_), None, _) => {
846+
return Err(KclError::new_semantic(KclErrorDetails::new(
847+
"`alignAxis` is required if `normalToFace` is specified.".to_owned(),
848+
vec![args.source_range],
849+
)));
850+
}
851+
(_, None, Some(_), _) => {
852+
return Err(KclError::new_semantic(KclErrorDetails::new(
853+
"`normalToFace` is required if `alignAxis` is specified.".to_owned(),
854+
vec![args.source_range],
855+
)));
856+
}
857+
(_, None, _, Some(_)) => {
858+
return Err(KclError::new_semantic(KclErrorDetails::new(
859+
"`normalToFace` is required if `normalOffset` is specified.".to_owned(),
860+
vec![args.source_range],
861+
)));
862+
}
863+
(_, Some(face), Some(_), _) => Some(face),
864+
(None, None, None, None) => None,
865+
};
866+
826867
match plane_or_solid {
827868
SketchData::PlaneOrientation(plane_data) => {
828869
let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
@@ -860,20 +901,93 @@ async fn inner_start_sketch_on(
860901
vec![args.source_range],
861902
)));
862903
};
863-
let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
904+
if let Some(align_axis) = align_axis {
905+
let plane_of = inner_plane_of(*solid, tag, exec_state, args).await?;
906+
907+
let offset = normal_offset.map_or(0.0, |x| x.n);
908+
let (x_axis, y_axis, normal_offset) = match align_axis {
909+
Axis2dOrEdgeReference::Axis { direction, origin: _ } => {
910+
if (direction[0].n - 1.0).abs() < f64::EPSILON {
911+
//X axis chosen
912+
(
913+
plane_of.info.x_axis,
914+
plane_of.info.z_axis,
915+
plane_of.info.y_axis * offset,
916+
)
917+
} else if (direction[0].n + 1.0).abs() < f64::EPSILON {
918+
// -X axis chosen
919+
(
920+
plane_of.info.x_axis.negated(),
921+
plane_of.info.z_axis,
922+
plane_of.info.y_axis * offset,
923+
)
924+
} else if (direction[1].n - 1.0).abs() < f64::EPSILON {
925+
// Y axis chosen
926+
(
927+
plane_of.info.y_axis,
928+
plane_of.info.z_axis,
929+
plane_of.info.x_axis * offset,
930+
)
931+
} else if (direction[1].n + 1.0).abs() < f64::EPSILON {
932+
// -Y axis chosen
933+
(
934+
plane_of.info.y_axis.negated(),
935+
plane_of.info.z_axis,
936+
plane_of.info.x_axis * offset,
937+
)
938+
} else {
939+
return Err(KclError::new_semantic(KclErrorDetails::new(
940+
"Unsupported axis detected. This function only supports using X, -X, Y and -Y."
941+
.to_owned(),
942+
vec![args.source_range],
943+
)));
944+
}
945+
}
946+
Axis2dOrEdgeReference::Edge(_) => {
947+
return Err(KclError::new_semantic(KclErrorDetails::new(
948+
"Use of an edge here is unsupported, please specify an `Axis2d` (e.g. `X`) instead."
949+
.to_owned(),
950+
vec![args.source_range],
951+
)));
952+
}
953+
};
954+
let origin = Point3d::new(0.0, 0.0, 0.0, plane_of.info.origin.units);
955+
let plane_data = PlaneData::Plane(PlaneInfo {
956+
origin: plane_of.project(origin) + normal_offset,
957+
x_axis,
958+
y_axis,
959+
z_axis: x_axis.axes_cross_product(&y_axis),
960+
});
961+
let plane = make_sketch_plane_from_orientation(plane_data, exec_state, args).await?;
864962

865-
#[cfg(feature = "artifact-graph")]
866-
{
867963
// Create artifact used only by the UI, not the engine.
868-
let id = exec_state.next_uuid();
869-
exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
870-
id: ArtifactId::from(id),
871-
face_id: face.artifact_id,
872-
code_ref: CodeRef::placeholder(args.source_range),
873-
}));
874-
}
964+
#[cfg(feature = "artifact-graph")]
965+
{
966+
let id = exec_state.next_uuid();
967+
exec_state.add_artifact(Artifact::StartSketchOnPlane(StartSketchOnPlane {
968+
id: ArtifactId::from(id),
969+
plane_id: plane.artifact_id,
970+
code_ref: CodeRef::placeholder(args.source_range),
971+
}));
972+
}
875973

876-
Ok(SketchSurface::Face(face))
974+
Ok(SketchSurface::Plane(plane))
975+
} else {
976+
let face = start_sketch_on_face(solid, tag, exec_state, args).await?;
977+
978+
#[cfg(feature = "artifact-graph")]
979+
{
980+
// Create artifact used only by the UI, not the engine.
981+
let id = exec_state.next_uuid();
982+
exec_state.add_artifact(Artifact::StartSketchOnFace(StartSketchOnFace {
983+
id: ArtifactId::from(id),
984+
face_id: face.artifact_id,
985+
code_ref: CodeRef::placeholder(args.source_range),
986+
}));
987+
}
988+
989+
Ok(SketchSurface::Face(face))
990+
}
877991
}
878992
}
879993
}

rust/kcl-lib/std/sketch.kcl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,36 @@
181181
/// |> close()
182182
/// |> extrude(length = 3.14)
183183
/// ```
184+
///
185+
/// ```kcl
186+
/// sketch001 = startSketchOn(XY)
187+
/// |> startProfile(at = [-5, -5])
188+
/// |> xLine(length = 10)
189+
/// |> yLine(length = 10, tag = $b)
190+
/// |> xLine(length = -10)
191+
/// |> close()
192+
///
193+
/// cube001 = extrude(sketch001, length = 10)
194+
/// |> rotate(roll = 10, pitch = 20, yaw = 30)
195+
///
196+
/// sketch002 = startSketchOn(cube001, normalToFace = END, alignAxis = Y)
197+
/// |> circle(radius = 2, center = [0, 0])
198+
/// cube002 = extrude(sketch002, length = 4)
199+
///
200+
/// subtract(cube001, tools = cube002)
201+
/// ```
184202
@(impl = std_rust)
185203
export fn startSketchOn(
186204
/// Profile whose start is being used.
187205
@planeOrSolid: Solid | Plane,
188-
/// Identify a face of a solid if a solid is specified as the input argument (`planeOrSolid`).
206+
/// Identify a face of a solid if a solid is specified as the input argument (`planeOrSolid`). Incompatible with `normalToFace`.
189207
face?: TaggedFace,
208+
/// Identify a face of a solid if a solid is specified as the input argument. Starts a sketch on the plane orthogonal to this specified face. Incompatible with `face`, requires `alignAxis`.
209+
normalToFace?: TaggedFace,
210+
/// If sketching normal to face, this axis will be the new local x axis of the sketch plane. The selected face's normal will be the local y axis. Incompatible with `face`, requires `normalToFace`.
211+
alignAxis?: Axis2d,
212+
/// Offset the sketch plane along its normal by the given amount. Incompatible with `face`, requires `normalToFace`.
213+
normalOffset?: number(Length),
190214
): Plane | Face {}
191215

192216
/// Start a new profile at a given point.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
sketch001 = startSketchOn(XY)
2+
|> startProfile(at = [-5, -5])
3+
|> xLine(length = 10, tag = $a)
4+
|> yLine(length = 10, tag = $b)
5+
|> xLine(length = -10, tag = $c)
6+
|> close()
7+
8+
cube001 = extrude(sketch001, length = 10)
9+
10+
sketch002 = startSketchOn(cube001, normalToFace = a, face = a)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
sketch001 = startSketchOn(XY)
2+
|> startProfile(at = [-5, -5])
3+
|> xLine(length = 10, tag = $a)
4+
|> yLine(length = 10, tag = $b)
5+
|> xLine(length = -10, tag = $c)
6+
|> close()
7+
8+
cube001 = extrude(sketch001, length = 10)
9+
10+
sketch002 = startSketchOn(cube001, normalToFace = a)
11+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
sketch001 = startSketchOn(XY)
2+
|> startProfile(at = [-5, -5])
3+
|> xLine(length = 10, tag = $a)
4+
|> yLine(length = 10, tag = $b)
5+
|> xLine(length = -10, tag = $c)
6+
|> close()
7+
8+
cube001 = extrude(sketch001, length = 10)
9+
10+
sketch002 = startSketchOn(cube001, alignAxis = X)
11+

0 commit comments

Comments
 (0)