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>
2964
docs/kcl/std.json
157
docs/kcl/std.md
@ -40,6 +40,7 @@
|
||||
* [`log2`](#log2)
|
||||
* [`max`](#max)
|
||||
* [`min`](#min)
|
||||
* [`patternLinear`](#patternLinear)
|
||||
* [`pi`](#pi)
|
||||
* [`pow`](#pow)
|
||||
* [`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
|
||||
|
||||
* `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.
|
||||
@ -2455,6 +2456,7 @@ hole(hole_sketch_group: SketchGroup, sketch_group: SketchGroup) -> SketchGroup
|
||||
// The to point.
|
||||
to: [number, number],
|
||||
},
|
||||
type: string,
|
||||
// The paths in the sketch group.
|
||||
value: [{
|
||||
// The from point.
|
||||
@ -2517,6 +2519,9 @@ hole(hole_sketch_group: SketchGroup, sketch_group: SketchGroup) -> SketchGroup
|
||||
yAxis: [number, number, number],
|
||||
// The z-axis of the sketch group base plane in the 3D space
|
||||
zAxis: [number, number, number],
|
||||
} |
|
||||
{
|
||||
type: string,
|
||||
}
|
||||
```
|
||||
* `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
|
||||
|
||||
Return the value of `pi`. Archimedes’ constant (π).
|
||||
|
@ -810,3 +810,42 @@ const part002 = startSketchOn('XY')
|
||||
|> 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1075,7 +1075,7 @@ export class EngineCommandManager {
|
||||
|
||||
if (command && command.type === 'pending') {
|
||||
const resolve = command.resolve
|
||||
this.artifactMap[id] = {
|
||||
const artifact = {
|
||||
type: 'result',
|
||||
range: command.range,
|
||||
pathToNode: command.pathToNode,
|
||||
@ -1083,6 +1083,13 @@ export class EngineCommandManager {
|
||||
parentId: command.parentId ? command.parentId : undefined,
|
||||
data: modelingResponse,
|
||||
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({
|
||||
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).
|
||||
// Using an array is the list is likely to grow.
|
||||
'start_path',
|
||||
'entity_linear_pattern',
|
||||
]
|
||||
if (!artifactTypesToDelete.includes(artifact.commandType)) return
|
||||
artifactsToDelete[id] = artifact
|
||||
if (artifactTypesToDelete.includes(artifact.commandType)) {
|
||||
artifactsToDelete[id] = artifact
|
||||
}
|
||||
})
|
||||
Object.keys(artifactsToDelete).forEach((id) => {
|
||||
const deletCmd: EngineCommand = {
|
||||
const deleteCmd: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
@ -1207,7 +1216,7 @@ export class EngineCommandManager {
|
||||
object_ids: [id],
|
||||
},
|
||||
}
|
||||
this.engineConnection?.send(deletCmd)
|
||||
this.engineConnection?.send(deleteCmd)
|
||||
})
|
||||
}
|
||||
addCommandLog(message: CommandLog) {
|
||||
|
@ -147,6 +147,7 @@ export const _executor = async (
|
||||
)
|
||||
return memory
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
const parsed: RustKclError = JSON.parse(e.toString())
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
|
@ -352,7 +352,7 @@ export function processCodeMirrorRanges({
|
||||
}
|
||||
})
|
||||
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
|
||||
const entriesWithOverlap = Object.entries(
|
||||
engineCommandManager.artifactMap || {}
|
||||
@ -362,8 +362,7 @@ export function processCodeMirrorRanges({
|
||||
: false
|
||||
})
|
||||
if (entriesWithOverlap.length) {
|
||||
const [id, artifact] = entriesWithOverlap?.[0]
|
||||
return {
|
||||
return entriesWithOverlap.map(([id, artifact]) => ({
|
||||
type,
|
||||
id:
|
||||
type === 'line-end' &&
|
||||
@ -371,7 +370,7 @@ export function processCodeMirrorRanges({
|
||||
artifact.headVertexId
|
||||
? artifact.headVertexId
|
||||
: id,
|
||||
}
|
||||
}))
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
@ -52,9 +52,9 @@ debug = true
|
||||
[workspace]
|
||||
members = [
|
||||
"derive-docs",
|
||||
"grackle",
|
||||
"grackle",
|
||||
"kcl",
|
||||
"kcl-macros",
|
||||
"kcl-macros",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
|
@ -6,7 +6,6 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
rust-version = "1.73"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
|
@ -3,7 +3,6 @@ name = "grackle"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
@ -14,7 +13,6 @@ kittycad-modeling-session = { workspace = true }
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["macros", "rt"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1"
|
||||
serde_json = "1.0.113"
|
||||
|
@ -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 kcl_lib::ast::types::{LiteralIdentifier, LiteralValue};
|
||||
|
||||
use super::{native_functions, Address};
|
||||
use crate::{CompileError, KclFunction};
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -17,9 +17,11 @@ use kittycad_execution_plan_traits as ept;
|
||||
use kittycad_execution_plan_traits::{Address, NumericPrimitive};
|
||||
use kittycad_modeling_session::Session;
|
||||
|
||||
use self::binding_scope::{BindingScope, EpBinding, GetFnResult};
|
||||
use self::error::{CompileError, Error};
|
||||
use self::kcl_value_group::SingleValue;
|
||||
use self::{
|
||||
binding_scope::{BindingScope, EpBinding, GetFnResult},
|
||||
error::{CompileError, Error},
|
||||
kcl_value_group::SingleValue,
|
||||
};
|
||||
|
||||
/// 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> {
|
||||
|
@ -5,7 +5,6 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
|
@ -8,7 +8,6 @@ repository = "https://github.com/KittyCAD/modeling-app"
|
||||
rust-version = "1.73"
|
||||
authors = ["Jess Frazelle", "Adam Chalmers", "KittyCAD, Inc"]
|
||||
keywords = ["kcl", "KittyCAD", "CAD"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
|
@ -10,8 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value as JValue};
|
||||
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, DocumentSymbol, Range as LspRange, SymbolKind};
|
||||
|
||||
pub use self::literal_value::LiteralValue;
|
||||
pub use self::none::KclNone;
|
||||
pub use self::{literal_value::LiteralValue, none::KclNone};
|
||||
use crate::{
|
||||
docs::StdLibFn,
|
||||
errors::{KclError, KclErrorDetails},
|
||||
|
@ -4,9 +4,8 @@ use databake::*;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::executor::{MemoryItem, SourceRange, UserVal};
|
||||
|
||||
use super::ConstraintLevel;
|
||||
use crate::executor::{MemoryItem, SourceRange, UserVal};
|
||||
|
||||
/// KCL value for an optional parameter which was not given an argument.
|
||||
/// (remember, parameters are in the function declaration,
|
||||
|
@ -69,13 +69,13 @@ impl EngineConnection {
|
||||
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 {
|
||||
let ToEngineReq { req, request_sent } = req;
|
||||
let res = if let kittycad::types::WebSocketRequest::ModelingCmdReq { cmd, cmd_id: _ } = &req {
|
||||
if let kittycad::types::ModelingCmd::ImportFiles { .. } = cmd {
|
||||
// Send it as binary.
|
||||
Self::inner_send_to_engine_binary(req, &mut tcp_write).await
|
||||
} else {
|
||||
Self::inner_send_to_engine(req, &mut tcp_write).await
|
||||
}
|
||||
let res = if let kittycad::types::WebSocketRequest::ModelingCmdReq {
|
||||
cmd: kittycad::types::ModelingCmd::ImportFiles { .. },
|
||||
cmd_id: _,
|
||||
} = &req
|
||||
{
|
||||
// Send it as binary.
|
||||
Self::inner_send_to_engine_binary(req, &mut tcp_write).await
|
||||
} else {
|
||||
Self::inner_send_to_engine(req, &mut tcp_write).await
|
||||
};
|
||||
|
@ -102,6 +102,7 @@ impl ProgramReturn {
|
||||
}
|
||||
}
|
||||
|
||||
/// A memory item.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
#[serde(tag = "type")]
|
||||
@ -109,7 +110,13 @@ pub enum MemoryItem {
|
||||
UserVal(UserVal),
|
||||
Plane(Box<Plane>),
|
||||
SketchGroup(Box<SketchGroup>),
|
||||
SketchGroups {
|
||||
value: Vec<Box<SketchGroup>>,
|
||||
},
|
||||
ExtrudeGroup(Box<ExtrudeGroup>),
|
||||
ExtrudeGroups {
|
||||
value: Vec<Box<ExtrudeGroup>>,
|
||||
},
|
||||
#[ts(skip)]
|
||||
ExtrudeTransform(Box<ExtrudeTransform>),
|
||||
#[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.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||
#[ts(export)]
|
||||
@ -212,7 +255,15 @@ impl From<MemoryItem> for Vec<SourceRange> {
|
||||
match item {
|
||||
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::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::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::Function { meta, .. } => 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 {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::ast::types::{Identifier, Parameter};
|
||||
|
||||
use super::*;
|
||||
use crate::ast::types::{Identifier, Parameter};
|
||||
|
||||
pub async fn parse_execute(code: &str) -> Result<ProgramMemory> {
|
||||
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":[]}"#);
|
||||
}
|
||||
}
|
||||
|
@ -47,7 +47,10 @@ async fn inner_extrude(length: f64, sketch_group: Box<SketchGroup>, args: Args)
|
||||
.await?;
|
||||
|
||||
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
|
||||
// comment was originally in the JS code.
|
||||
value: Default::default(),
|
||||
|
@ -3,6 +3,7 @@
|
||||
pub mod extrude;
|
||||
pub mod kcl_stdlib;
|
||||
pub mod math;
|
||||
pub mod patterns;
|
||||
pub mod segment;
|
||||
pub mod shapes;
|
||||
pub mod sketch;
|
||||
@ -24,7 +25,9 @@ use crate::{
|
||||
docs::StdLibFn,
|
||||
engine::EngineManager,
|
||||
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>>>>;
|
||||
@ -67,6 +70,7 @@ lazy_static! {
|
||||
Box::new(crate::std::sketch::TangentialArcTo),
|
||||
Box::new(crate::std::sketch::BezierCurve),
|
||||
Box::new(crate::std::sketch::Hole),
|
||||
Box::new(crate::std::patterns::PatternLinear),
|
||||
Box::new(crate::std::math::Cos),
|
||||
Box::new(crate::std::math::Sin),
|
||||
Box::new(crate::std::math::Tan),
|
||||
@ -284,7 +288,7 @@ impl Args {
|
||||
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(|| {
|
||||
KclError::Type(KclErrorDetails {
|
||||
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 {
|
||||
sg.clone()
|
||||
let sketch_set = if let MemoryItem::SketchGroup(sg) = first_value {
|
||||
SketchGroupSet::SketchGroup(sg.clone())
|
||||
} else if let MemoryItem::SketchGroups { value } = first_value {
|
||||
SketchGroupSet::SketchGroups(value.clone())
|
||||
} else {
|
||||
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],
|
||||
}));
|
||||
};
|
||||
@ -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()
|
||||
} else {
|
||||
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> {
|
||||
@ -400,6 +409,49 @@ impl Args {
|
||||
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> {
|
||||
let first_value = self
|
||||
.args
|
||||
@ -741,8 +793,6 @@ mod tests {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -756,8 +806,6 @@ mod tests {
|
||||
let internal_fn = stdlib.fns.get(key).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(
|
||||
"../../../docs/kcl/std.json",
|
||||
&serde_json::to_string_pretty(&json_data).unwrap(),
|
||||
|
101
src/wasm-lib/kcl/src/std/patterns.rs
Normal 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)
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
//! Functions related to sketching.
|
||||
|
||||
use crate::std::utils::{get_tangent_point_from_previous_arc, get_tangential_arc_to_info, TangentialArcInfoInput};
|
||||
use anyhow::Result;
|
||||
use derive_docs::stdlib;
|
||||
use kittycad::types::{Angle, ModelingCmd, Point3D};
|
||||
@ -8,14 +7,17 @@ use kittycad_execution_plan_macros::ExecutionPlanValue;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::executor::SourceRange;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::{
|
||||
BasePath, GeoMeta, MemoryItem, Path, Plane, PlaneType, Point2d, Point3d, Position, Rotation, SketchGroup,
|
||||
SketchGroupSet, SourceRange,
|
||||
},
|
||||
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,
|
||||
},
|
||||
};
|
||||
@ -1419,7 +1421,7 @@ async fn inner_bezier_curve(
|
||||
|
||||
/// Use a sketch to cut a hole in another sketch.
|
||||
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?;
|
||||
Ok(MemoryItem::SketchGroup(new_sketch_group))
|
||||
@ -1430,31 +1432,57 @@ pub async fn hole(args: Args) -> Result<MemoryItem, KclError> {
|
||||
name = "hole",
|
||||
}]
|
||||
async fn inner_hole(
|
||||
hole_sketch_group: Box<SketchGroup>,
|
||||
hole_sketch_group: SketchGroupSet,
|
||||
sketch_group: Box<SketchGroup>,
|
||||
args: Args,
|
||||
) -> Result<Box<SketchGroup>, KclError> {
|
||||
//TODO: batch these (once we have batch)
|
||||
|
||||
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?;
|
||||
match hole_sketch_group {
|
||||
SketchGroupSet::SketchGroup(hole_sketch_group) => {
|
||||
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?;
|
||||
}
|
||||
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?
|
||||
|
||||
|
@ -638,9 +638,10 @@ pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialAr
|
||||
|
||||
#[cfg(test)]
|
||||
mod get_tangential_arc_to_info_tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn round_to_three_decimals(num: f64) -> f64 {
|
||||
(num * 1000.0).round() / 1000.0
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use winnow::{
|
||||
error::{ContextError, ParseError},
|
||||
prelude::*,
|
||||
stream::{Location, Stream},
|
||||
token::{any, none_of, one_of, take_till1, take_until0},
|
||||
token::{any, none_of, one_of, take_till, take_until},
|
||||
Located,
|
||||
};
|
||||
|
||||
@ -47,13 +47,13 @@ pub fn token(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)?;
|
||||
Ok(Token::from_range(range, TokenType::BlockComment, value.to_string()))
|
||||
}
|
||||
|
||||
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)?;
|
||||
Ok(Token::from_range(range, TokenType::LineComment, value.to_string()))
|
||||
}
|
||||
|
@ -559,3 +559,136 @@ circle([0,0], 22) |> extrude(14, %)"#;
|
||||
let result = execute_and_snapshot(code).await.unwrap();
|
||||
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);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
BIN
src/wasm-lib/tests/executor/outputs/patterns_linear_basic.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
src/wasm-lib/tests/executor/outputs/patterns_linear_basic_3d.png
Normal file
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 83 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 71 KiB |