Linear patterns (#1362)

* add linear patterns

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add failing test for serialisation issue

* cleanup tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup memoryitem

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add test to serialize memory item from rust

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* update test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* run cargo sort everywhere

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* run fmt everywhere

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix typo

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clean up linear paterns on re-execute

* selections fix for patterns

* fix clippy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
Jess Frazelle
2024-02-11 15:08:54 -08:00
committed by GitHub
parent 8378eb1e94
commit b94c5be1af
30 changed files with 3170 additions and 531 deletions

File diff suppressed because it is too large Load Diff

View File

@ -40,6 +40,7 @@
* [`log2`](#log2) * [`log2`](#log2)
* [`max`](#max) * [`max`](#max)
* [`min`](#min) * [`min`](#min)
* [`patternLinear`](#patternLinear)
* [`pi`](#pi) * [`pi`](#pi)
* [`pow`](#pow) * [`pow`](#pow)
* [`segAng`](#segAng) * [`segAng`](#segAng)
@ -2430,12 +2431,12 @@ Use a sketch to cut a hole in another sketch.
``` ```
hole(hole_sketch_group: SketchGroup, sketch_group: SketchGroup) -> SketchGroup hole(hole_sketch_group: SketchGroupSet, sketch_group: SketchGroup) -> SketchGroup
``` ```
#### Arguments #### Arguments
* `hole_sketch_group`: `SketchGroup` - A sketch group is a collection of paths. * `hole_sketch_group`: `SketchGroupSet` - A sketch group or a group of sketch groups.
``` ```
{ {
// The id of the sketch group. // The id of the sketch group.
@ -2455,6 +2456,7 @@ hole(hole_sketch_group: SketchGroup, sketch_group: SketchGroup) -> SketchGroup
// The to point. // The to point.
to: [number, number], to: [number, number],
}, },
type: string,
// The paths in the sketch group. // The paths in the sketch group.
value: [{ value: [{
// The from point. // The from point.
@ -2517,6 +2519,9 @@ hole(hole_sketch_group: SketchGroup, sketch_group: SketchGroup) -> SketchGroup
yAxis: [number, number, number], yAxis: [number, number, number],
// The z-axis of the sketch group base plane in the 3D space // The z-axis of the sketch group base plane in the 3D space
zAxis: [number, number, number], zAxis: [number, number, number],
} |
{
type: string,
} }
``` ```
* `sketch_group`: `SketchGroup` - A sketch group is a collection of paths. * `sketch_group`: `SketchGroup` - A sketch group is a collection of paths.
@ -3475,6 +3480,154 @@ min(args: [number]) -> number
### patternLinear
A linear pattern.
```
patternLinear(data: LinearPatternData, geometry: Geometry) -> Geometries
```
#### Arguments
* `data`: `LinearPatternData` - Data for a linear pattern.
```
{
// The axis of the pattern. This is a 3D vector.
axis: [number, number, number],
// The distance between each repetition. This can also be referred to as spacing.
distance: number,
// The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once.
repetitions: number,
}
```
* `geometry`: `Geometry` - A geometry.
```
{
// The id of the sketch group.
id: uuid,
// The plane id of the sketch group.
planeId: uuid,
// The position of the sketch group.
position: [number, number, number],
// The rotation of the sketch group base plane.
rotation: [number, number, number, number],
// The starting path.
start: {
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
},
type: string,
// The paths in the sketch group.
value: [{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: string,
} |
{
// arc's direction
ccw: string,
// the arc's center
center: [number, number],
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: string,
} |
{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: string,
// The x coordinate.
x: number,
} |
{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: string,
// The x coordinate.
x: number,
// The y coordinate.
y: number,
} |
{
// The from point.
from: [number, number],
// The name of the path.
name: string,
// The to point.
to: [number, number],
type: string,
}],
// The x-axis of the sketch group base plane in the 3D space
xAxis: [number, number, number],
// The y-axis of the sketch group base plane in the 3D space
yAxis: [number, number, number],
// The z-axis of the sketch group base plane in the 3D space
zAxis: [number, number, number],
} |
{
// The height of the extrude group.
height: number,
// The id of the extrude group.
id: uuid,
// The position of the extrude group.
position: [number, number, number],
// The rotation of the extrude group.
rotation: [number, number, number, number],
type: string,
// The extrude surfaces.
value: [{
// The id of the geometry.
id: uuid,
// The name.
name: string,
// The position.
position: [number, number, number],
// The rotation.
rotation: [number, number, number, number],
// The source range.
sourceRange: [number, number],
type: string,
}],
}
```
#### Returns
* `Geometries` - A set of geometry.
```
{
type: string,
} |
{
type: string,
}
```
### pi ### pi
Return the value of `pi`. Archimedes constant (π). Return the value of `pi`. Archimedes constant (π).

View File

@ -810,3 +810,42 @@ const part002 = startSketchOn('XY')
|> line([-47.44, 0], %)`.replace(/\s/g, '') |> line([-47.44, 0], %)`.replace(/\s/g, '')
) )
}) })
test('ProgramMemory can be serialised', async ({ page, context }) => {
const u = getUtils(page)
await context.addInitScript(async (token) => {
localStorage.setItem(
'persistCode',
`const part = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
|> extrude(1, %)
|> patternLinear({
axis: [1, 0, 1],
repetitions: 3,
distance: 6
}, %)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await page.goto('/')
const messages: string[] = []
// Listen for all console events and push the message text to an array
page.on('console', (message) => messages.push(message.text()))
await u.waitForAuthSkipAppStart()
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
const forbiddenMessages = ['cannot serialize tagged newtype variant']
forbiddenMessages.forEach((forbiddenMessage) => {
messages.forEach((message) => {
expect(message).not.toContain(forbiddenMessage)
})
})
})

View File

@ -1075,7 +1075,7 @@ export class EngineCommandManager {
if (command && command.type === 'pending') { if (command && command.type === 'pending') {
const resolve = command.resolve const resolve = command.resolve
this.artifactMap[id] = { const artifact = {
type: 'result', type: 'result',
range: command.range, range: command.range,
pathToNode: command.pathToNode, pathToNode: command.pathToNode,
@ -1083,6 +1083,13 @@ export class EngineCommandManager {
parentId: command.parentId ? command.parentId : undefined, parentId: command.parentId ? command.parentId : undefined,
data: modelingResponse, data: modelingResponse,
raw: message, raw: message,
} as const
this.artifactMap[id] = artifact
if (command.commandType === 'entity_linear_pattern') {
const entities = (modelingResponse as any)?.data?.entity_ids
entities?.forEach((entity: string) => {
this.artifactMap[entity] = artifact
})
} }
resolve({ resolve({
id, id,
@ -1194,12 +1201,14 @@ export class EngineCommandManager {
// this fact is very opaque in the api and docs (as to what should can be deleted). // this fact is very opaque in the api and docs (as to what should can be deleted).
// Using an array is the list is likely to grow. // Using an array is the list is likely to grow.
'start_path', 'start_path',
'entity_linear_pattern',
] ]
if (!artifactTypesToDelete.includes(artifact.commandType)) return if (artifactTypesToDelete.includes(artifact.commandType)) {
artifactsToDelete[id] = artifact artifactsToDelete[id] = artifact
}
}) })
Object.keys(artifactsToDelete).forEach((id) => { Object.keys(artifactsToDelete).forEach((id) => {
const deletCmd: EngineCommand = { const deleteCmd: EngineCommand = {
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
@ -1207,7 +1216,7 @@ export class EngineCommandManager {
object_ids: [id], object_ids: [id],
}, },
} }
this.engineConnection?.send(deletCmd) this.engineConnection?.send(deleteCmd)
}) })
} }
addCommandLog(message: CommandLog) { addCommandLog(message: CommandLog) {

View File

@ -147,6 +147,7 @@ export const _executor = async (
) )
return memory return memory
} catch (e: any) { } catch (e: any) {
console.log(e)
const parsed: RustKclError = JSON.parse(e.toString()) const parsed: RustKclError = JSON.parse(e.toString())
const kclError = new KCLError( const kclError = new KCLError(
parsed.kind, parsed.kind,

View File

@ -352,7 +352,7 @@ export function processCodeMirrorRanges({
} }
}) })
const idBasedSelections: SelectionToEngine[] = codeBasedSelections const idBasedSelections: SelectionToEngine[] = codeBasedSelections
.map(({ type, range }): null | SelectionToEngine => { .flatMap(({ type, range }): null | SelectionToEngine[] => {
// TODO #868: loops over all artifacts will become inefficient at a large scale // TODO #868: loops over all artifacts will become inefficient at a large scale
const entriesWithOverlap = Object.entries( const entriesWithOverlap = Object.entries(
engineCommandManager.artifactMap || {} engineCommandManager.artifactMap || {}
@ -362,8 +362,7 @@ export function processCodeMirrorRanges({
: false : false
}) })
if (entriesWithOverlap.length) { if (entriesWithOverlap.length) {
const [id, artifact] = entriesWithOverlap?.[0] return entriesWithOverlap.map(([id, artifact]) => ({
return {
type, type,
id: id:
type === 'line-end' && type === 'line-end' &&
@ -371,7 +370,7 @@ export function processCodeMirrorRanges({
artifact.headVertexId artifact.headVertexId
? artifact.headVertexId ? artifact.headVertexId
: id, : id,
} }))
} }
return null return null
}) })

View File

@ -6,7 +6,6 @@ edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
rust-version = "1.73" rust-version = "1.73"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]

View File

@ -3,7 +3,6 @@ name = "grackle"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "A new executor for KCL which compiles to Execution Plans" description = "A new executor for KCL which compiles to Execution Plans"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
@ -14,7 +13,6 @@ kittycad-modeling-session = { workspace = true }
thiserror = "1.0.57" thiserror = "1.0.57"
tokio = { version = "1.36.0", features = ["macros", "rt"] } tokio = { version = "1.36.0", features = ["macros", "rt"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1" pretty_assertions = "1"
serde_json = "1.0.113" serde_json = "1.0.113"

View File

@ -1,14 +1,10 @@
use kcl_lib::ast::types::LiteralIdentifier;
use kcl_lib::ast::types::LiteralValue;
use crate::CompileError;
use crate::KclFunction;
use super::native_functions;
use super::Address;
use std::collections::HashMap; use std::collections::HashMap;
use kcl_lib::ast::types::{LiteralIdentifier, LiteralValue};
use super::{native_functions, Address};
use crate::{CompileError, KclFunction};
/// KCL values which can be written to KCEP memory. /// KCL values which can be written to KCEP memory.
/// This is recursive. For example, the bound value might be an array, which itself contains bound values. /// This is recursive. For example, the bound value might be an array, which itself contains bound values.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -17,9 +17,11 @@ use kittycad_execution_plan_traits as ept;
use kittycad_execution_plan_traits::{Address, NumericPrimitive}; use kittycad_execution_plan_traits::{Address, NumericPrimitive};
use kittycad_modeling_session::Session; use kittycad_modeling_session::Session;
use self::binding_scope::{BindingScope, EpBinding, GetFnResult}; use self::{
use self::error::{CompileError, Error}; binding_scope::{BindingScope, EpBinding, GetFnResult},
use self::kcl_value_group::SingleValue; error::{CompileError, Error},
kcl_value_group::SingleValue,
};
/// Execute a KCL program by compiling into an execution plan, then running that. /// Execute a KCL program by compiling into an execution plan, then running that.
pub async fn execute(ast: Program, session: Option<Session>) -> Result<ep::Memory, Error> { pub async fn execute(ast: Program, session: Option<Session>) -> Result<ep::Memory, Error> {

View File

@ -5,7 +5,6 @@ version = "0.1.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]

View File

@ -8,7 +8,6 @@ repository = "https://github.com/KittyCAD/modeling-app"
rust-version = "1.73" rust-version = "1.73"
authors = ["Jess Frazelle", "Adam Chalmers", "KittyCAD, Inc"] authors = ["Jess Frazelle", "Adam Chalmers", "KittyCAD, Inc"]
keywords = ["kcl", "KittyCAD", "CAD"] keywords = ["kcl", "KittyCAD", "CAD"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]

View File

@ -10,8 +10,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JValue}; use serde_json::{Map, Value as JValue};
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, Range as LspRange, SymbolKind}; use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, Range as LspRange, SymbolKind};
pub use self::literal_value::LiteralValue; pub use self::{literal_value::LiteralValue, none::KclNone};
pub use self::none::KclNone;
use crate::{ use crate::{
docs::StdLibFn, docs::StdLibFn,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},

View File

@ -4,9 +4,8 @@ use databake::*;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::executor::{MemoryItem, SourceRange, UserVal};
use super::ConstraintLevel; use super::ConstraintLevel;
use crate::executor::{MemoryItem, SourceRange, UserVal};
/// KCL value for an optional parameter which was not given an argument. /// KCL value for an optional parameter which was not given an argument.
/// (remember, parameters are in the function declaration, /// (remember, parameters are in the function declaration,

View File

@ -69,15 +69,15 @@ impl EngineConnection {
async fn start_write_actor(mut tcp_write: WebSocketTcpWrite, mut engine_req_rx: mpsc::Receiver<ToEngineReq>) { async fn start_write_actor(mut tcp_write: WebSocketTcpWrite, mut engine_req_rx: mpsc::Receiver<ToEngineReq>) {
while let Some(req) = engine_req_rx.recv().await { while let Some(req) = engine_req_rx.recv().await {
let ToEngineReq { req, request_sent } = req; let ToEngineReq { req, request_sent } = req;
let res = if let kittycad::types::WebSocketRequest::ModelingCmdReq { cmd, cmd_id: _ } = &req { let res = if let kittycad::types::WebSocketRequest::ModelingCmdReq {
if let kittycad::types::ModelingCmd::ImportFiles { .. } = cmd { cmd: kittycad::types::ModelingCmd::ImportFiles { .. },
cmd_id: _,
} = &req
{
// Send it as binary. // Send it as binary.
Self::inner_send_to_engine_binary(req, &mut tcp_write).await Self::inner_send_to_engine_binary(req, &mut tcp_write).await
} else { } else {
Self::inner_send_to_engine(req, &mut tcp_write).await Self::inner_send_to_engine(req, &mut tcp_write).await
}
} else {
Self::inner_send_to_engine(req, &mut tcp_write).await
}; };
let _ = request_sent.send(res); let _ = request_sent.send(res);
} }

View File

@ -102,6 +102,7 @@ impl ProgramReturn {
} }
} }
/// A memory item.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -109,7 +110,13 @@ pub enum MemoryItem {
UserVal(UserVal), UserVal(UserVal),
Plane(Box<Plane>), Plane(Box<Plane>),
SketchGroup(Box<SketchGroup>), SketchGroup(Box<SketchGroup>),
SketchGroups {
value: Vec<Box<SketchGroup>>,
},
ExtrudeGroup(Box<ExtrudeGroup>), ExtrudeGroup(Box<ExtrudeGroup>),
ExtrudeGroups {
value: Vec<Box<ExtrudeGroup>>,
},
#[ts(skip)] #[ts(skip)]
ExtrudeTransform(Box<ExtrudeTransform>), ExtrudeTransform(Box<ExtrudeTransform>),
#[ts(skip)] #[ts(skip)]
@ -122,6 +129,42 @@ pub enum MemoryItem {
}, },
} }
/// A geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Geometry {
SketchGroup(Box<SketchGroup>),
ExtrudeGroup(Box<ExtrudeGroup>),
}
impl Geometry {
pub fn id(&self) -> uuid::Uuid {
match self {
Geometry::SketchGroup(s) => s.id,
Geometry::ExtrudeGroup(e) => e.id,
}
}
}
/// A set of geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum Geometries {
SketchGroups(Vec<Box<SketchGroup>>),
ExtrudeGroups(Vec<Box<ExtrudeGroup>>),
}
/// A sketch group or a group of sketch groups.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SketchGroupSet {
SketchGroup(Box<SketchGroup>),
SketchGroups(Vec<Box<SketchGroup>>),
}
/// A plane. /// A plane.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
@ -212,7 +255,15 @@ impl From<MemoryItem> for Vec<SourceRange> {
match item { match item {
MemoryItem::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(), MemoryItem::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(), MemoryItem::SketchGroup(s) => s.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::SketchGroups { value } => value
.iter()
.flat_map(|sg| sg.meta.iter().map(|m| m.source_range))
.collect(),
MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(), MemoryItem::ExtrudeGroup(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::ExtrudeGroups { value } => value
.iter()
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(),
MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(), MemoryItem::ExtrudeTransform(e) => e.meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(), MemoryItem::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
MemoryItem::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(), MemoryItem::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
@ -1040,9 +1091,8 @@ fn assign_args_to_params(
mod tests { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::ast::types::{Identifier, Parameter};
use super::*; use super::*;
use crate::ast::types::{Identifier, Parameter};
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> { pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
let tokens = crate::token::lexer(code); let tokens = crate::token::lexer(code);
@ -1703,4 +1753,13 @@ show(bracket)
); );
} }
} }
#[test]
fn test_serialize_memory_item() {
let mem = MemoryItem::ExtrudeGroups {
value: Default::default(),
};
let json = serde_json::to_string(&mem).unwrap();
assert_eq!(json, r#"{"type":"ExtrudeGroups","value":[]}"#);
}
} }

View File

@ -47,7 +47,10 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
.await?; .await?;
Ok(Box::new(ExtrudeGroup { Ok(Box::new(ExtrudeGroup {
id, // Ok so you would think that the id would be the id of the extrude group,
// that we passed in to the function, but it's actually the id of the
// sketch group.
id: sketch_group.id,
// TODO, this is just an empty array now, should be deleted. This // TODO, this is just an empty array now, should be deleted. This
// comment was originally in the JS code. // comment was originally in the JS code.
value: Default::default(), value: Default::default(),

View File

@ -3,6 +3,7 @@
pub mod extrude; pub mod extrude;
pub mod kcl_stdlib; pub mod kcl_stdlib;
pub mod math; pub mod math;
pub mod patterns;
pub mod segment; pub mod segment;
pub mod shapes; pub mod shapes;
pub mod sketch; pub mod sketch;
@ -24,7 +25,9 @@ use crate::{
docs::StdLibFn, docs::StdLibFn,
engine::EngineManager, engine::EngineManager,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ExecutorContext, ExtrudeGroup, MemoryItem, Metadata, Plane, SketchGroup, SourceRange}, executor::{
ExecutorContext, ExtrudeGroup, Geometry, MemoryItem, Metadata, Plane, SketchGroup, SketchGroupSet, SourceRange,
},
}; };
pub type StdFn = fn(Args) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<MemoryItem, KclError>>>>; pub type StdFn = fn(Args) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<MemoryItem, KclError>>>>;
@ -67,6 +70,7 @@ lazy_static! {
Box::new(crate::std::sketch::TangentialArcTo), Box::new(crate::std::sketch::TangentialArcTo),
Box::new(crate::std::sketch::BezierCurve), Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Hole), Box::new(crate::std::sketch::Hole),
Box::new(crate::std::patterns::PatternLinear),
Box::new(crate::std::math::Cos), Box::new(crate::std::math::Cos),
Box::new(crate::std::math::Sin), Box::new(crate::std::math::Sin),
Box::new(crate::std::math::Tan), Box::new(crate::std::math::Tan),
@ -284,7 +288,7 @@ impl Args {
Ok((segment_name, sketch_group)) Ok((segment_name, sketch_group))
} }
fn get_sketch_groups(&self) -> Result<(Box<SketchGroup>, Box<SketchGroup>), KclError> { fn get_sketch_groups(&self) -> Result<(SketchGroupSet, Box<SketchGroup>), KclError> {
let first_value = self.args.first().ok_or_else(|| { let first_value = self.args.first().ok_or_else(|| {
KclError::Type(KclErrorDetails { KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args), message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args),
@ -292,11 +296,16 @@ impl Args {
}) })
})?; })?;
let sketch_group = if let MemoryItem::SketchGroup(sg) = first_value { let sketch_set = if let MemoryItem::SketchGroup(sg) = first_value {
sg.clone() SketchGroupSet::SketchGroup(sg.clone())
} else if let MemoryItem::SketchGroups { value } = first_value {
SketchGroupSet::SketchGroups(value.clone())
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the first argument, found `{:?}`", self.args), message: format!(
"Expected a SketchGroup or Vector of SketchGroups as the first argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range], source_ranges: vec![self.source_range],
})); }));
}; };
@ -308,7 +317,7 @@ impl Args {
}) })
})?; })?;
let second_sketch_group = if let MemoryItem::SketchGroup(sg) = second_value { let sketch_group = if let MemoryItem::SketchGroup(sg) = second_value {
sg.clone() sg.clone()
} else { } else {
return Err(KclError::Type(KclErrorDetails { return Err(KclError::Type(KclErrorDetails {
@ -317,7 +326,7 @@ impl Args {
})); }));
}; };
Ok((sketch_group, second_sketch_group)) Ok((sketch_set, sketch_group))
} }
fn get_sketch_group(&self) -> Result<Box<SketchGroup>, KclError> { fn get_sketch_group(&self) -> Result<Box<SketchGroup>, KclError> {
@ -400,6 +409,49 @@ impl Args {
Ok((data, sketch_group)) Ok((data, sketch_group))
} }
fn get_data_and_geometry<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Geometry), KclError> {
let first_value = self
.args
.first()
.ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a struct as the first argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?
.get_json_value()?;
let data: T = serde_json::from_value(first_value).map_err(|e| {
KclError::Type(KclErrorDetails {
message: format!("Failed to deserialize struct from JSON: {}", e),
source_ranges: vec![self.source_range],
})
})?;
let second_value = self.args.get(1).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Expected a SketchGroup as the second argument, found `{:?}`", self.args),
source_ranges: vec![self.source_range],
})
})?;
let geometry = if let MemoryItem::SketchGroup(sg) = second_value {
Geometry::SketchGroup(sg.clone())
} else if let MemoryItem::ExtrudeGroup(eg) = second_value {
Geometry::ExtrudeGroup(eg.clone())
} else {
return Err(KclError::Type(KclErrorDetails {
message: format!(
"Expected a SketchGroup or ExtrudeGroup as the second argument, found `{:?}`",
self.args
),
source_ranges: vec![self.source_range],
}));
};
Ok((data, geometry))
}
fn get_data_and_plane<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<Plane>), KclError> { fn get_data_and_plane<T: serde::de::DeserializeOwned>(&self) -> Result<(T, Box<Plane>), KclError> {
let first_value = self let first_value = self
.args .args
@ -741,8 +793,6 @@ mod tests {
buf.push_str(&fn_docs); buf.push_str(&fn_docs);
} }
// uncomment to update
// std::fs::write("../../../docs/kcl/std.md", &buf).expect("Unable to write to file");
expectorate::assert_contents("../../../docs/kcl/std.md", &buf); expectorate::assert_contents("../../../docs/kcl/std.md", &buf);
} }
@ -756,8 +806,6 @@ mod tests {
let internal_fn = stdlib.fns.get(key).unwrap(); let internal_fn = stdlib.fns.get(key).unwrap();
json_data.push(internal_fn.to_json().unwrap()); json_data.push(internal_fn.to_json().unwrap());
} }
// uncomment to update
// std::fs::write("../../../docs/kcl/std.json", json_output).expect("Unable to write to file");
expectorate::assert_contents( expectorate::assert_contents(
"../../../docs/kcl/std.json", "../../../docs/kcl/std.json",
&serde_json::to_string_pretty(&json_data).unwrap(), &serde_json::to_string_pretty(&json_data).unwrap(),

View File

@ -0,0 +1,101 @@
//! Standard library patterns.
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{Geometries, Geometry, MemoryItem},
std::Args,
};
/// Data for a linear pattern.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LinearPatternData {
/// The number of repetitions. Must be greater than 0.
/// This excludes the original entity. For example, if `repetitions` is 1,
/// the original entity will be copied once.
pub repetitions: usize,
/// The distance between each repetition. This can also be referred to as spacing.
pub distance: f64,
/// The axis of the pattern. This is a 3D vector.
pub axis: [f64; 3],
}
/// A linear pattern.
pub async fn pattern_linear(args: Args) -> Result<MemoryItem, KclError> {
let (data, geometry): (LinearPatternData, Geometry) = args.get_data_and_geometry()?;
if data.axis == [0.0, 0.0, 0.0] {
return Err(KclError::Semantic(KclErrorDetails {
message:
"The axis of the linear pattern cannot be the zero vector. Otherwise they will just duplicate in place."
.to_string(),
source_ranges: vec![args.source_range],
}));
}
let new_geometries = inner_pattern_linear(data, geometry, args).await?;
match new_geometries {
Geometries::SketchGroups(sketch_groups) => Ok(MemoryItem::SketchGroups { value: sketch_groups }),
Geometries::ExtrudeGroups(extrude_groups) => Ok(MemoryItem::ExtrudeGroups { value: extrude_groups }),
}
}
/// A linear pattern.
#[stdlib {
name = "patternLinear",
}]
async fn inner_pattern_linear(data: LinearPatternData, geometry: Geometry, args: Args) -> Result<Geometries, KclError> {
let id = uuid::Uuid::new_v4();
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::EntityLinearPattern {
axis: data.axis.into(),
entity_id: geometry.id(),
num_repetitions: data.repetitions as u32,
spacing: data.distance,
},
)
.await?;
let kittycad::types::OkWebSocketResponseData::Modeling {
modeling_response: kittycad::types::OkModelingCmdResponse::EntityLinearPattern { data: pattern_info },
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("EntityLinearPattern response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
let geometries = match geometry {
Geometry::SketchGroup(sketch_group) => {
let mut geometries = vec![sketch_group.clone()];
for id in pattern_info.entity_ids.iter() {
let mut new_sketch_group = sketch_group.clone();
new_sketch_group.id = *id;
geometries.push(new_sketch_group);
}
Geometries::SketchGroups(geometries)
}
Geometry::ExtrudeGroup(extrude_group) => {
let mut geometries = vec![extrude_group.clone()];
for id in pattern_info.entity_ids.iter() {
let mut new_extrude_group = extrude_group.clone();
new_extrude_group.id = *id;
geometries.push(new_extrude_group);
}
Geometries::ExtrudeGroups(geometries)
}
};
Ok(geometries)
}

View File

@ -1,6 +1,5 @@
//! Functions related to sketching. //! Functions related to sketching.
use crate::std::utils::{get_tangent_point_from_previous_arc, get_tangential_arc_to_info, TangentialArcInfoInput};
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kittycad::types::{Angle, ModelingCmd, Point3D}; use kittycad::types::{Angle, ModelingCmd, Point3D};
@ -8,14 +7,17 @@ use kittycad_execution_plan_macros::ExecutionPlanValue;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::executor::SourceRange;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::{ executor::{
BasePath, GeoMeta, MemoryItem, Path, Plane, PlaneType, Point2d, Point3d, Position, Rotation, SketchGroup, BasePath, GeoMeta, MemoryItem, Path, Plane, PlaneType, Point2d, Point3d, Position, Rotation, SketchGroup,
SketchGroupSet, SourceRange,
}, },
std::{ std::{
utils::{arc_angles, arc_center_and_end, get_x_component, get_y_component, intersection_with_parallel_line}, utils::{
arc_angles, arc_center_and_end, get_tangent_point_from_previous_arc, get_tangential_arc_to_info,
get_x_component, get_y_component, intersection_with_parallel_line, TangentialArcInfoInput,
},
Args, Args,
}, },
}; };
@ -1419,7 +1421,7 @@ async fn inner_bezier_curve(
/// Use a sketch to cut a hole in another sketch. /// Use a sketch to cut a hole in another sketch.
pub async fn hole(args: Args) -> Result<MemoryItem, KclError> { pub async fn hole(args: Args) -> Result<MemoryItem, KclError> {
let (hole_sketch_group, sketch_group): (Box<SketchGroup>, Box<SketchGroup>) = args.get_sketch_groups()?; let (hole_sketch_group, sketch_group): (SketchGroupSet, Box<SketchGroup>) = args.get_sketch_groups()?;
let new_sketch_group = inner_hole(hole_sketch_group, sketch_group, args).await?; let new_sketch_group = inner_hole(hole_sketch_group, sketch_group, args).await?;
Ok(MemoryItem::SketchGroup(new_sketch_group)) Ok(MemoryItem::SketchGroup(new_sketch_group))
@ -1430,12 +1432,14 @@ pub async fn hole(args: Args) -> Result<MemoryItem, KclError> {
name = "hole", name = "hole",
}] }]
async fn inner_hole( async fn inner_hole(
hole_sketch_group: Box<SketchGroup>, hole_sketch_group: SketchGroupSet,
sketch_group: Box<SketchGroup>, sketch_group: Box<SketchGroup>,
args: Args, args: Args,
) -> Result<Box<SketchGroup>, KclError> { ) -> Result<Box<SketchGroup>, KclError> {
//TODO: batch these (once we have batch) //TODO: batch these (once we have batch)
match hole_sketch_group {
SketchGroupSet::SketchGroup(hole_sketch_group) => {
args.send_modeling_cmd( args.send_modeling_cmd(
uuid::Uuid::new_v4(), uuid::Uuid::new_v4(),
ModelingCmd::Solid2DAddHole { ModelingCmd::Solid2DAddHole {
@ -1444,7 +1448,6 @@ async fn inner_hole(
}, },
) )
.await?; .await?;
// suggestion (mike) // suggestion (mike)
// we also hide the source hole since its essentially "consumed" by this operation // we also hide the source hole since its essentially "consumed" by this operation
args.send_modeling_cmd( args.send_modeling_cmd(
@ -1455,6 +1458,31 @@ async fn inner_hole(
}, },
) )
.await?; .await?;
}
SketchGroupSet::SketchGroups(hole_sketch_groups) => {
for hole_sketch_group in hole_sketch_groups {
println!("hole_sketch_group: {:?} {}", hole_sketch_group.id, sketch_group.id);
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::Solid2DAddHole {
object_id: sketch_group.id,
hole_id: hole_sketch_group.id,
},
)
.await?;
// suggestion (mike)
// we also hide the source hole since its essentially "consumed" by this operation
args.send_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::ObjectVisible {
object_id: hole_sketch_group.id,
hidden: true,
},
)
.await?;
}
}
}
// TODO: should we modify the sketch group to include the hole data, probably? // TODO: should we modify the sketch group to include the hole data, probably?

View File

@ -638,9 +638,10 @@ pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialAr
#[cfg(test)] #[cfg(test)]
mod get_tangential_arc_to_info_tests { mod get_tangential_arc_to_info_tests {
use super::*;
use approx::assert_relative_eq; use approx::assert_relative_eq;
use super::*;
fn round_to_three_decimals(num: f64) -> f64 { fn round_to_three_decimals(num: f64) -> f64 {
(num * 1000.0).round() / 1000.0 (num * 1000.0).round() / 1000.0
} }

View File

@ -4,7 +4,7 @@ use winnow::{
error::{ContextError, ParseError}, error::{ContextError, ParseError},
prelude::*, prelude::*,
stream::{Location, Stream}, stream::{Location, Stream},
token::{any, none_of, one_of, take_till1, take_until0}, token::{any, none_of, one_of, take_till, take_until},
Located, Located,
}; };
@ -47,13 +47,13 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
} }
fn block_comment(i: &mut Located<&str>) -> PResult<Token> { fn block_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = ("/*", take_until0("*/"), "*/").recognize(); let inner = ("/*", take_until(0.., "*/"), "*/").recognize();
let (value, range) = inner.with_span().parse_next(i)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::BlockComment, value.to_string())) Ok(Token::from_range(range, TokenType::BlockComment, value.to_string()))
} }
fn line_comment(i: &mut Located<&str>) -> PResult<Token> { fn line_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = (r#"//"#, take_till1(['\n', '\r'])).recognize(); let inner = (r#"//"#, take_till(1.., ['\n', '\r'])).recognize();
let (value, range) = inner.with_span().parse_next(i)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::LineComment, value.to_string())) Ok(Token::from_range(range, TokenType::LineComment, value.to_string()))
} }

View File

@ -559,3 +559,136 @@ circle([0,0], 22) |> extrude(14, %)"#;
let result = execute_and_snapshot(code).await.unwrap(); let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, 0.999); twenty_twenty::assert_image("tests/executor/outputs/top_level_expression.png", &result, 0.999);
} }
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_linear_basic() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = circle([0,0], 2)
|> patternLinear({axis: [0,0,1], repetitions: 12, distance: 2}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_linear_basic_3d() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0,1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
|> extrude(1, %)
|> patternLinear({axis: [1, 0,1], repetitions: 3, distance: 6}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_3d.png", &result, 0.999);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_linear_basic_negative_distance() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = circle([0,0], 2)
|> patternLinear({axis: [0,0,1], repetitions: 12, distance: -2}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_negative_distance.png",
&result,
0.999,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_linear_basic_negative_axis() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const part = circle([0,0], 2)
|> patternLinear({axis: [0,0,-1], repetitions: 12, distance: 2}, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image(
"tests/executor/outputs/patterns_linear_basic_negative_axis.png",
&result,
0.999,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn serial_test_patterns_linear_basic_holes() {
let code = r#"fn circle = (pos, radius) => {
const sg = startSketchOn('XY')
|> startProfileAt([pos[0] + radius, pos[1]], %)
|> arc({
angle_end: 360,
angle_start: 0,
radius: radius
}, %)
|> close(%)
return sg
}
const circles = circle([5, 5], 1)
|> patternLinear({axis: [1,1,0], repetitions: 12, distance: 3}, %)
const rectangle = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 50], %)
|> line([50, 0], %)
|> line([0, -50], %)
|> close(%)
|> hole(circles, %)
"#;
let result = execute_and_snapshot(code).await.unwrap();
twenty_twenty::assert_image("tests/executor/outputs/patterns_linear_basic_holes.png", &result, 0.999);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB