Merge branch 'main' into lf94/kurt-bring-back-multi-profile

This commit is contained in:
49lf
2025-01-16 11:13:59 -05:00
33 changed files with 642 additions and 280 deletions

View File

@ -53,7 +53,6 @@ layout: manual
* [`hollow`](kcl/hollow) * [`hollow`](kcl/hollow)
* [`import`](kcl/import) * [`import`](kcl/import)
* [`inch`](kcl/inch) * [`inch`](kcl/inch)
* [`int`](kcl/int)
* [`lastSegX`](kcl/lastSegX) * [`lastSegX`](kcl/lastSegX)
* [`lastSegY`](kcl/lastSegY) * [`lastSegY`](kcl/lastSegY)
* [`legAngX`](kcl/legAngX) * [`legAngX`](kcl/legAngX)

View File

@ -4,6 +4,8 @@ excerpt: "Convert a number to an integer."
layout: manual layout: manual
--- ---
**WARNING:** This function is deprecated.
Convert a number to an integer. Convert a number to an integer.
DEPRECATED use floor(), ceil(), or round(). DEPRECATED use floor(), ceil(), or round().

View File

@ -87204,7 +87204,7 @@
"labelRequired": true "labelRequired": true
}, },
"unpublished": false, "unpublished": false,
"deprecated": false, "deprecated": true,
"examples": [ "examples": [
"n = int(ceil(5 / 2))\nassertEqual(n, 3, 0.0001, \"5/2 = 2.5, rounded up makes 3\")\n// Draw n cylinders.\nstartSketchOn('XZ')\n |> circle({ center = [0, 0], radius = 2 }, %)\n |> extrude(5, %)\n |> patternTransform(n, fn(id) {\n return { translate = [4 * id, 0, 0] }\n }, %)" "n = int(ceil(5 / 2))\nassertEqual(n, 3, 0.0001, \"5/2 = 2.5, rounded up makes 3\")\n// Draw n cylinders.\nstartSketchOn('XZ')\n |> circle({ center = [0, 0], radius = 2 }, %)\n |> extrude(5, %)\n |> patternTransform(n, fn(id) {\n return { translate = [4 * id, 0, 0] }\n }, %)"
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -8,8 +8,8 @@ import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
test.describe('Testing in-app sample loading', () => { test.describe('Testing in-app sample loading', () => {
/** /**
* Note this test implicitly depends on the KCL sample "car-wheel.kcl", * Note this test implicitly depends on the KCL sample "a-parametric-bearing-pillow-block",
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl * its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl
*/ */
test('Web: should overwrite current code, cannot create new file', async ({ test('Web: should overwrite current code, cannot create new file', async ({
editor, editor,
@ -29,8 +29,8 @@ test.describe('Testing in-app sample loading', () => {
// Locators and constants // Locators and constants
const newSample = { const newSample = {
file: 'car-wheel' + FILE_EXT, file: 'a-parametric-bearing-pillow-block' + FILE_EXT,
title: 'Car Wheel', title: 'A Parametric Bearing Pillow Block',
} }
const commandBarButton = page.getByRole('button', { name: 'Commands' }) const commandBarButton = page.getByRole('button', { name: 'Commands' })
const samplesCommandOption = page.getByRole('option', { const samplesCommandOption = page.getByRole('option', {
@ -75,8 +75,8 @@ test.describe('Testing in-app sample loading', () => {
/** /**
* Note this test implicitly depends on the KCL samples: * Note this test implicitly depends on the KCL samples:
* "car-wheel.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl * "a-parametric-bearing-pillow-block": https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl
* "gear-rack.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/gear-rack.kcl * "gear-rack": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/main.kcl
*/ */
test( test(
'Desktop: should create new file by default, optionally overwrite', 'Desktop: should create new file by default, optionally overwrite',
@ -93,8 +93,8 @@ test.describe('Testing in-app sample loading', () => {
// Locators and constants // Locators and constants
const sampleOne = { const sampleOne = {
file: 'car-wheel' + FILE_EXT, file: 'a-parametric-bearing-pillow-block' + FILE_EXT,
title: 'Car Wheel', title: 'A Parametric Bearing Pillow Block',
} }
const sampleTwo = { const sampleTwo = {
file: 'gear-rack' + FILE_EXT, file: 'gear-rack' + FILE_EXT,

View File

@ -1,151 +1,211 @@
[ [
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
"multipleFiles": false,
"title": "80/20 Rail", "title": "80/20 Rail",
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position" "description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
"multipleFiles": false,
"title": "A Parametric Bearing Pillow Block", "title": "A Parametric Bearing Pillow Block",
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads." "description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
"multipleFiles": false,
"title": "Ball Bearing", "title": "Ball Bearing",
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads." "description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
"multipleFiles": false,
"title": "Shelf Bracket", "title": "Shelf Bracket",
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided." "description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
}, },
{ {
"file": "brake-caliper.kcl", "file": "main.kcl",
"title": "Brake Caliper", "pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
"description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed." "multipleFiles": true,
"title": "Car Wheel Assembly",
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
"multipleFiles": false,
"title": "Hollow Dodecahedron", "title": "Hollow Dodecahedron",
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards." "description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
"multipleFiles": false,
"title": "Enclosure", "title": "Enclosure",
"description": "An enclosure body and sealing lid for storing items" "description": "An enclosure body and sealing lid for storing items"
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
"multipleFiles": false,
"title": "Flange", "title": "Flange",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others." "description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
"multipleFiles": false,
"title": "Flange with XY coordinates", "title": "Flange with XY coordinates",
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others." "description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
"multipleFiles": false,
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface", "title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material" "description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
"multipleFiles": false,
"title": "Food Service Spatula", "title": "Food Service Spatula",
"description": "Use these spatulas for mixing, flipping, and scraping." "description": "Use these spatulas for mixing, flipping, and scraping."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
"multipleFiles": false,
"title": "French Press", "title": "French Press",
"description": "A french press immersion coffee maker" "description": "A french press immersion coffee maker"
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
"multipleFiles": false,
"title": "Spur Gear", "title": "Spur Gear",
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear." "description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
"multipleFiles": false,
"title": "100mm Gear Rack", "title": "100mm Gear Rack",
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate." "description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
"multipleFiles": false,
"title": "Hex nut", "title": "Hex nut",
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware." "description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
"multipleFiles": false,
"title": "I-beam", "title": "I-beam",
"description": "A structural metal beam with an I shaped cross section. Often used in construction" "description": "A structural metal beam with an I shaped cross section. Often used in construction"
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
"multipleFiles": false,
"title": "Kitt", "title": "Kitt",
"description": "The beloved KittyCAD mascot in a voxelized style." "description": "The beloved KittyCAD mascot in a voxelized style."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
"multipleFiles": false,
"title": "Lego Brick", "title": "Lego Brick",
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code." "description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
"multipleFiles": false,
"title": "Mounting Plate", "title": "Mounting Plate",
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components." "description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
}, },
{ {
"file": "globals.kcl", "file": "main.kcl",
"title": "Global constants for the multi-axis robot", "pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
"description": "" "multipleFiles": true,
"title": "Robot Arm",
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
"multipleFiles": false,
"title": "Pipe", "title": "Pipe",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow." "description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
"multipleFiles": false,
"title": "Pipe and Flange Assembly", "title": "Pipe and Flange Assembly",
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint." "description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
"multipleFiles": false,
"title": "Pipe with bend", "title": "Pipe with bend",
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow." "description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
"multipleFiles": false,
"title": "Poopy Shoe", "title": "Poopy Shoe",
"description": "poop shute for bambu labs printer - optimized for printing." "description": "poop shute for bambu labs printer - optimized for printing."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
"multipleFiles": false,
"title": "Router template for a cross bar", "title": "Router template for a cross bar",
"description": "A guide for routing a notch into a cross bar." "description": "A guide for routing a notch into a cross bar."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
"multipleFiles": false,
"title": "Router template for a slate", "title": "Router template for a slate",
"description": "A guide for routing a slate for a cross bar." "description": "A guide for routing a slate for a cross bar."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
"multipleFiles": false,
"title": "Sheet Metal Bracket", "title": "Sheet Metal Bracket",
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly." "description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
"multipleFiles": false,
"title": "Socket Head Cap Screw", "title": "Socket Head Cap Screw",
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key." "description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
}, },
{ {
"file": "antenna.kcl", "file": "main.kcl",
"title": "Antenna", "pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
"description": "" "multipleFiles": true,
"title": "Walkie Talkie",
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
}, },
{ {
"file": "main.kcl", "file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
"multipleFiles": false,
"title": "Washer", "title": "Washer",
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time." "description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
} }

View File

@ -1,5 +1,6 @@
import { import {
BoxGeometry, BoxGeometry,
Color,
DoubleSide, DoubleSide,
Group, Group,
Intersection, Intersection,
@ -57,6 +58,7 @@ import {
resultIsOk, resultIsOk,
SourceRange, SourceRange,
} from 'lang/wasm' } from 'lang/wasm'
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
import { import {
engineCommandManager, engineCommandManager,
kclManager, kclManager,
@ -68,7 +70,7 @@ import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { executeAst, ToolTip } from 'lang/langHelpers' import { executeAst, ToolTip } from 'lang/langHelpers'
import { import {
createProfileStartHandle, createProfileStartHandle,
createArcGeometry, createCircleGeometry,
SegmentUtils, SegmentUtils,
segmentUtils, segmentUtils,
} from './segments' } from './segments'
@ -116,6 +118,8 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d' import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
import { SegmentInputs } from 'lang/std/stdTypes' import { SegmentInputs } from 'lang/std/stdTypes'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
import { radToDeg } from 'three/src/math/MathUtils' import { radToDeg } from 'three/src/math/MathUtils'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph' import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
@ -1412,110 +1416,98 @@ export class SceneEntities {
const groupOfDrafts = new Group() const groupOfDrafts = new Group()
groupOfDrafts.name = 'circle-3-point-group' groupOfDrafts.name = 'circle-3-point-group'
groupOfDrafts.position.copy(sketchOrigin) groupOfDrafts.position.copy(sketchOrigin)
// lee: I'm keeping this here as a developer gotchya: // lee: I'm keeping this here as a developer gotchya:
// Do not reorient your surfaces to the intersection plane. Your points are // If you use 3D points, do not rotate anything.
// already in 3D space, not 2D. If you intersect say XZ, you want the points // If you use 2D points (easier to deal with, generally do this!), then
// to continue to live at the 3D intersection point, not be rotated to end // rotate the group just like this! Remember to rotate other groups too!
// up elsewhere! groupOfDrafts.setRotationFromQuaternion(orientation)
// groupOfDrafts.setRotationFromQuaternion(orientation)
this.scene.add(groupOfDrafts) this.scene.add(groupOfDrafts)
const DRAFT_POINT_RADIUS = 6 // How large the points on the circle will render as
const DRAFT_POINT_RADIUS = 10 // px
const createPoint = (center: Vector3): number => { // The target of our dragging
let target: Object3D | undefined = undefined
// The KCL this will generate.
const kclCircle3Point = parse(`circleThreePoint(
p1 = [0.0, 0.0],
p2 = [0.0, 0.0],
p3 = [0.0, 0.0],
)`)
const createPoint = (
center: Vector3,
opts?: { noInteraction?: boolean }
): Mesh => {
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS) const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
const color = getThemeColorForThreeJs(sceneInfra._theme) const color = getThemeColorForThreeJs(sceneInfra._theme)
const material = new MeshBasicMaterial({ color })
const material = new MeshBasicMaterial({
color: opts?.noInteraction
? sceneInfra._theme === 'light'
? new Color(color).multiplyScalar(0.15)
: new Color(0x010101).multiplyScalar(2000)
: color,
})
const mesh = new Mesh(geometry, material) const mesh = new Mesh(geometry, material)
mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT } mesh.userData = {
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
}
mesh.renderOrder = 1000
mesh.layers.set(SKETCH_LAYER) mesh.layers.set(SKETCH_LAYER)
mesh.position.copy(center) mesh.position.copy(center)
mesh.scale.set(scale, scale, scale) mesh.scale.set(scale, scale, scale)
mesh.renderOrder = 100 mesh.renderOrder = 100
groupOfDrafts.add(mesh) return mesh
return mesh.id
} }
const circle3Point = ( const createCircle3PointGraphic = async (
points: Vector2[] points: Vector2[],
): undefined | { center: Vector3; radius: number } => { center: Vector2,
// A 3-point circle is undefined if it doesn't have 3 points :) radius: number
if (points.length !== 3) return undefined ) => {
if (
// y = (i/j)(x-h) + b Number.isNaN(radius) ||
// i and j variables for the slopes Number.isNaN(center.x) ||
const i = [points[1].x - points[0].x, points[2].x - points[1].x] Number.isNaN(center.y)
const j = [points[1].y - points[0].y, points[2].y - points[1].y] )
return
// Our / threejs coordinate system affects this a lot. If you take this
// code into a different code base, you may have to adjust a/b to being
// -1/a/b, b/a, etc! In this case, a/-b did the trick.
const m = [i[0] / -j[0], i[1] / -j[1]]
const h = [
(points[0].x + points[1].x) / 2,
(points[1].x + points[2].x) / 2,
]
const b = [
(points[0].y + points[1].y) / 2,
(points[1].y + points[2].y) / 2,
]
// Algebraically derived
const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0])
const y = m[0] * (x - h[0]) + b[0]
const center = new Vector3(x, y, 0)
const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2)
return {
center,
radius,
}
}
// TO BE SHORT LIVED: unused function to draw the circle and lines.
// @ts-ignore
// eslint-disable-next-line
const createCircle3Point = (points: Vector2[]) => {
const circleParams = circle3Point(points)
// A circle cannot be created for these points.
if (!circleParams) return
const color = getThemeColorForThreeJs(sceneInfra._theme) const color = getThemeColorForThreeJs(sceneInfra._theme)
const geometryCircle = createArcGeometry({ const lineCircle = createCircleGeometry({
center: [circleParams.center.x, circleParams.center.y], center: [center.x, center.y],
radius: circleParams.radius, radius,
startAngle: 0, color,
endAngle: Math.PI * 2, isDashed: false,
ccw: true, scale: 1,
isDashed: true,
scale,
}) })
const materialCircle = new MeshBasicMaterial({ color }) lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
lineCircle.layers.set(SKETCH_LAYER)
// devnote: it's a mistake to use these with EllipseCurve :)
// lineCircle.position.set(center.x, center.y, 0)
// lineCircle.scale.set(scale, scale, scale)
if (groupCircle) groupOfDrafts.remove(groupCircle) if (groupCircle) groupOfDrafts.remove(groupCircle)
groupCircle = new Group() groupCircle = new Group()
groupCircle.renderOrder = 1 groupCircle.renderOrder = 1
groupCircle.add(lineCircle)
const meshCircle = new Mesh(geometryCircle, materialCircle) const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE } noInteraction: true,
meshCircle.layers.set(SKETCH_LAYER) })
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0) groupCircle.add(pointMesh)
meshCircle.scale.set(scale, scale, scale)
groupCircle.add(meshCircle)
const geometryPolyLine = new BufferGeometry().setFromPoints([ const geometryPolyLine = new BufferGeometry().setFromPoints([
...points, ...points.map((p) => new Vector3(p.x, p.y, 0)),
points[0], new Vector3(points[0].x, points[0].y, 0),
]) ])
const materialPolyLine = new LineDashedMaterial({ const materialPolyLine = new LineDashedMaterial({
color, color,
scale, scale: 1 / scale,
dashSize: 6, dashSize: 6,
gapSize: 6, gapSize: 6,
}) })
@ -1526,13 +1518,146 @@ export class SceneEntities {
groupOfDrafts.add(groupCircle) groupOfDrafts.add(groupCircle)
} }
// The target of our dragging const insertCircle3PointKclIntoAstSnapshot = (
let target: Object3D | undefined = undefined points: Vector2[]
): Program => {
if (err(kclCircle3Point) || kclCircle3Point.program === null)
return kclManager.ast
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return kclManager.ast
if (
kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw'
)
return kclManager.ast
const arg = (x: LabeledArg): Literal[] | undefined => {
if (
'arg' in x &&
'elements' in x.arg &&
x.arg.type === 'ArrayExpression'
) {
if (x.arg.elements.every((x) => x.type === 'Literal')) {
return x.arg.elements
}
}
return undefined
}
const kclCircle3PointArgs =
kclCircle3Point.program.body[0].expression.arguments
const arg0 = arg(kclCircle3PointArgs[0])
if (!arg0) return kclManager.ast
arg0[0].value = points[0].x
arg0[0].raw = points[0].x.toString()
arg0[1].value = points[0].y
arg0[1].raw = points[0].y.toString()
const arg1 = arg(kclCircle3PointArgs[1])
if (!arg1) return kclManager.ast
arg1[0].value = points[1].x
arg1[0].raw = points[1].x.toString()
arg1[1].value = points[1].y
arg1[1].raw = points[1].y.toString()
const arg2 = arg(kclCircle3PointArgs[2])
if (!arg2) return kclManager.ast
arg2[0].value = points[2].x
arg2[0].raw = points[2].x.toString()
arg2[1].value = points[2].y
arg2[1].raw = points[2].y.toString()
const astSnapshot = structuredClone(kclManager.ast)
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(startSketchOnASTNode)) return astSnapshot
// It's possible we're already dealing with a PipeExpression.
// Modify the current one.
if (
startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' &&
startSketchOnASTNode.node.declaration.init.body[1].type ===
'CallExpressionKw' &&
startSketchOnASTNode.node.declaration.init.body.length >= 2
) {
startSketchOnASTNode.node.declaration.init.body[1].arguments =
kclCircle3Point.program.body[0].expression.arguments
} else {
// Clone a new node based on the old, and replace the old with the new.
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
}
// Return the `Program`
return astSnapshot
}
const updateCircle3Point = async (opts?: { execute?: true }) => {
const points_ = Array.from(points.values())
const circleParams = calculate_circle_from_3_points(
points_[0].x,
points_[0].y,
points_[1].x,
points_[1].y,
points_[2].x,
points_[2].y
)
if (Number.isNaN(circleParams.radius)) return
await createCircle3PointGraphic(
points_,
new Vector2(circleParams.center_x, circleParams.center_y),
circleParams.radius
)
const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_)
const codeAsString = recast(astWithNewCode)
if (err(codeAsString)) return
codeManager.updateCodeStateEditor(codeAsString)
}
const cleanupFn = () => { const cleanupFn = () => {
this.scene.remove(groupOfDrafts) this.scene.remove(groupOfDrafts)
} }
// The AST node we extracted earlier may already have a circleThreePoint!
// Use the points in the AST as starting points.
const astSnapshot = structuredClone(kclManager.ast)
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
)
if (err(maybeVariableDeclaration))
return () => {
done()
}
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
if (
maybeCallExpressionKw.type === 'PipeExpression' &&
maybeCallExpressionKw.body[1].type === 'CallExpressionKw' &&
maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint'
) {
maybeCallExpressionKw?.body[1].arguments
.map(
({ arg }: any) =>
new Vector2(arg.elements[0].value, arg.elements[1].value)
)
.forEach((point: Vector2) => {
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
groupOfDrafts.add(pointMesh)
points.set(pointMesh.id, point)
})
void updateCircle3Point()
}
sceneInfra.setCallbacks({ sceneInfra.setCallbacks({
async onDrag(args) { async onDrag(args) {
const draftPointsIntersected = args.intersects.filter( const draftPointsIntersected = args.intersects.filter(
@ -1548,8 +1673,18 @@ export class SceneEntities {
// The user was off their mark! Missed the object to select. // The user was off their mark! Missed the object to select.
if (!target) return if (!target) return
target.position.copy(args.intersectionPoint.threeD) target.position.copy(
new Vector3(
args.intersectionPoint.twoD.x,
args.intersectionPoint.twoD.y,
0
)
)
points.set(target.id, args.intersectionPoint.twoD) points.set(target.id, args.intersectionPoint.twoD)
if (points.size <= 2) return
await updateCircle3Point()
}, },
async onDragEnd(_args) { async onDragEnd(_args) {
target = undefined target = undefined
@ -1558,45 +1693,19 @@ export class SceneEntities {
if (points.size >= 3) return if (points.size >= 3) return
if (!args.intersectionPoint) return if (!args.intersectionPoint) return
const id = createPoint(args.intersectionPoint.threeD) const pointMesh = createPoint(
points.set(id, args.intersectionPoint.twoD) new Vector3(
args.intersectionPoint.twoD.x,
if (points.size < 2) return args.intersectionPoint.twoD.y,
0
// We've now got 3 points, let's create our circle! )
const astSnapshot = structuredClone(kclManager.ast)
let nodeQueryResult
nodeQueryResult = getNodeFromPath<VariableDeclaration>(
astSnapshot,
startSketchOnASTNodePath,
'VariableDeclaration'
) )
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult) groupOfDrafts.add(pointMesh)
const startSketchOnASTNode = nodeQueryResult points.set(pointMesh.id, args.intersectionPoint.twoD)
const circleParams = circle3Point(Array.from(points.values())) if (points.size <= 2) return
if (!circleParams) return await updateCircle3Point()
const kclCircle3Point = parse(`circle({
center = [${circleParams.center.x}, ${circleParams.center.y}],
radius = ${circleParams.radius},
}, %)`)
if (err(kclCircle3Point) || kclCircle3Point.program === null) return
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
return
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
startSketchOnASTNode.node.declaration.init = createPipeExpression([
clonedStartSketchOnASTNode.node.declaration.init,
kclCircle3Point.program.body[0].expression,
])
await kclManager.executeAstMock(astSnapshot)
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
done()
}, },
}) })

View File

@ -9,6 +9,9 @@ import {
ExtrudeGeometry, ExtrudeGeometry,
Group, Group,
LineCurve3, LineCurve3,
LineBasicMaterial,
LineDashedMaterial,
Line,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
NormalBufferAttributes, NormalBufferAttributes,
@ -1005,6 +1008,49 @@ export function createArcGeometry({
return geo return geo
} }
// (lee) The above is much more complex than necessary.
// I've derived the new code from:
// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve
// I'm not sure why it wasn't done like this in the first place?
// I don't touch the code above because it may break something else.
export function createCircleGeometry({
center,
radius,
color,
isDashed = false,
scale = 1,
}: {
center: Coords2d
radius: number
color: number
isDashed?: boolean
scale?: number
}): Line {
const circle = new EllipseCurve(
center[0],
center[1],
radius,
radius,
0,
Math.PI * 2,
true,
scale
)
const points = circle.getPoints(75) // just enough points to not see edges.
const geometry = new BufferGeometry().setFromPoints(points)
const material = !isDashed
? new LineBasicMaterial({ color })
: new LineDashedMaterial({
color,
scale,
dashSize: 6,
gapSize: 6,
})
const line = new Line(geometry, material)
line.computeLineDistances()
return line
}
export function dashedStraight( export function dashedStraight(
from: Coords2d, from: Coords2d,
to: Coords2d, to: Coords2d,

View File

@ -538,6 +538,16 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
helix: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.3796 6.35525C10.6758 5.64945 8.44129 5.27796 6.92519 5.64172C6.15726 5.82597 5.7318 6.05228 5.55779 6.21295C5.76304 6.32354 6.2288 6.43945 7.03653 6.43302C7.87009 6.42638 8.9975 6.29045 10.4229 5.9501L10.6551 6.92275C9.17724 7.27564 7.9725 7.42559 7.04449 7.43298C6.14216 7.44017 5.42343 7.31395 4.98579 7.03617C4.75792 6.89153 4.53857 6.65945 4.50435 6.32695C4.47054 5.99852 4.63374 5.72683 4.81912 5.53684C5.17998 5.16702 5.83926 4.87389 6.69188 4.66932C8.48928 4.23806 10.9508 4.68095 12.7623 5.43139C13.669 5.80697 14.4784 6.28567 14.9739 6.82869C15.2234 7.10197 15.4238 7.42493 15.4827 7.78937C15.5448 8.1741 15.4392 8.54567 15.1831 8.86785C14.9896 9.11133 14.6502 9.31092 14.327 9.47089C14.1575 9.55477 13.9707 9.63785 13.7736 9.71907C14.257 9.99254 14.6732 10.2984 14.9739 10.6279C15.2234 10.9011 15.4238 11.2241 15.4827 11.5885C15.5448 11.9733 15.4392 12.3448 15.1831 12.667C14.9896 12.9105 14.6502 13.1101 14.327 13.2701C14.1575 13.3539 13.9707 13.437 13.7735 13.5182C14.3755 13.8587 14.8991 14.2636 15.2067 14.7211L14.3767 15.2789C14.1912 15.0029 13.8109 14.6842 13.2483 14.3702C13.0112 14.2378 12.7496 14.1107 12.4694 13.9913C11.8027 14.2087 11.1417 14.3953 10.6642 14.5188L10.6552 14.5212L10.6551 14.5211C9.17724 14.874 7.9725 15.0239 7.04449 15.0313C6.14216 15.0385 5.42343 14.9123 4.98579 14.6345C4.75792 14.4899 4.53857 14.2578 4.50435 13.9253C4.47054 13.5969 4.63374 13.3252 4.81912 13.1352C5.17998 12.7653 5.83926 12.4722 6.69188 12.2677C8.12302 11.9243 9.96538 12.1368 11.5511 12.6039C11.5872 12.6145 11.6233 12.6253 11.6593 12.6363L10.0638 13.2745C8.93153 13.0645 7.80454 13.0291 6.92519 13.2401C6.15727 13.4243 5.73181 13.6506 5.5578 13.8113C5.76305 13.9219 6.2288 14.0378 7.03653 14.0313C7.8692 14.0247 8.99509 13.8891 10.4183 13.5495C10.5419 13.5175 10.678 13.4812 10.8233 13.4412C10.8184 13.4399 10.8134 13.4387 10.8085 13.4374L12.6 12.9L12.5922 12.8948C12.6584 12.8718 12.7243 12.8485 12.7894 12.825C13.2047 12.6754 13.5845 12.5217 13.8834 12.3738C14.2059 12.2142 14.359 12.0967 14.4003 12.0448C14.4964 11.9239 14.509 11.832 14.4955 11.748C14.4786 11.6437 14.4094 11.4927 14.2353 11.302C13.8963 10.9305 13.2766 10.536 12.4694 10.1921C11.8027 10.4096 11.1417 10.5962 10.6642 10.7197L10.6552 10.722L10.6551 10.7219C9.17724 11.0748 7.9725 11.2248 7.04449 11.2322C6.14216 11.2393 5.42343 11.1131 4.98579 10.8353C4.75792 10.6907 4.53857 10.4586 4.50435 10.1261C4.47054 9.79768 4.63374 9.526 4.81912 9.33601C5.17998 8.96618 5.83926 8.67306 6.69188 8.46848C8.14467 8.11991 10.0313 8.34242 11.6579 8.83682L10.0624 9.47503C8.9375 9.26666 7.80922 9.22878 6.92519 9.44089C6.15726 9.62514 5.7318 9.85144 5.55779 10.0121C5.76304 10.1227 6.2288 10.2386 7.03653 10.2322C7.86921 10.2255 8.9951 10.0899 10.4183 9.75035C10.542 9.71834 10.6781 9.68201 10.8235 9.64197L10.8072 9.63784L12.6 9.1L12.593 9.09536C12.659 9.0724 12.7245 9.04921 12.7894 9.02583C13.2047 8.87627 13.5845 8.72256 13.8834 8.57464C14.2059 8.41505 14.359 8.29757 14.4003 8.24564C14.4964 8.1247 14.509 8.0328 14.4955 7.94882C14.4786 7.84455 14.4094 7.69357 14.2353 7.50279C13.8839 7.11769 13.2307 6.7078 12.3796 6.35525ZM5.47539 9.95537C5.47546 9.95536 5.47623 9.95615 5.47745 9.95779C5.47592 9.9562 5.47531 9.95538 5.47539 9.95537ZM5.49369 10.0846C5.49289 10.0866 5.49232 10.0876 5.49223 10.0876C5.49215 10.0877 5.49255 10.0866 5.49369 10.0846ZM5.47539 13.7545C5.47546 13.7545 5.47623 13.7553 5.47745 13.757C5.47592 13.7554 5.47531 13.7546 5.47539 13.7545ZM5.49369 13.8838C5.49289 13.8858 5.49232 13.8868 5.49223 13.8868C5.49215 13.8868 5.49255 13.8858 5.49369 13.8838ZM5.47539 6.1562C5.47546 6.15619 5.47623 6.15698 5.47745 6.15862C5.47592 6.15704 5.47531 6.15622 5.47539 6.1562ZM5.49369 6.28544C5.49289 6.28746 5.49232 6.28848 5.49223 6.28848C5.49215 6.28849 5.49255 6.28748 5.49369 6.28544Z"
fill="currentColor"
/>
</svg>
),
hole: ( hole: (
<svg <svg
viewBox="0 0 20 20" viewBox="0 0 20 20"

View File

@ -57,7 +57,9 @@ export const FileMachineProvider = ({
useEffect(() => { useEffect(() => {
markOnce('code/didLoadFile') markOnce('code/didLoadFile')
async function fetchKclSamples() { async function fetchKclSamples() {
setKclSamples(await getKclSamplesManifest()) const manifest = await getKclSamplesManifest()
const filteredFiles = manifest.filter((file) => !file.multipleFiles)
setKclSamples(filteredFiles)
} }
fetchKclSamples().catch(reportError) fetchKclSamples().catch(reportError)
}, []) }, [])
@ -324,7 +326,7 @@ export const FileMachineProvider = ({
} }
}, },
kclSamples.map((sample) => ({ kclSamples.map((sample) => ({
value: sample.file, value: sample.pathFromProjectDirectoryToFirstFile,
name: sample.title, name: sample.title,
})) }))
).filter( ).filter(

View File

@ -1,79 +1,81 @@
import { assertParse, initPromise, programMemoryInit } from './wasm' import { assertParse, initPromise, programMemoryInit } from './wasm'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
// These unit tests makes web requests to a public github repository.
import path from 'node:path'
import fs from 'node:fs/promises'
import child_process from 'node:child_process'
// The purpose of these tests is to act as a first line of defense
// if something gets real screwy with our KCL ecosystem.
// THESE TESTS ONLY RUN UNDER A NODEJS ENVIRONMENT. They DO NOT
// test under our application.
const DIR_KCL_SAMPLES = 'kcl-samples'
const URL_GIT_KCL_SAMPLES = 'https://github.com/KittyCAD/kcl-samples.git'
interface KclSampleFile { interface KclSampleFile {
file: string file: string
pathFromProjectDirectoryToFirstFile: string
title: string title: string
filename: string filename: string
description: string description: string
} }
try {
// @ts-expect-error
await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
} catch (e) {
console.log(e)
}
child_process.spawnSync('git', ['clone', URL_GIT_KCL_SAMPLES, DIR_KCL_SAMPLES])
// @ts-expect-error
let files = await fs.readdir(DIR_KCL_SAMPLES)
// @ts-expect-error
const manifestJsonStr = await fs.readFile(
path.resolve(DIR_KCL_SAMPLES, 'manifest.json'),
'utf-8'
)
const manifest = JSON.parse(manifestJsonStr)
process.chdir(DIR_KCL_SAMPLES)
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
}) })
// Only used to actually fetch an older version of KCL code that will break in the parser. afterAll(async () => {
/* eslint-disable @typescript-eslint/no-unused-vars */ try {
async function getBrokenSampleCodeForLocalTesting() { process.chdir('..')
const result = await fetch( await fs.rm(DIR_KCL_SAMPLES, { recursive: true })
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/5ccd04a1773ebdbfd02684057917ce5dbe0eaab3/80-20-rail.kcl' } catch (e) {}
) })
const text = await result.text()
return text
}
async function getKclSampleCodeFromGithub(file: string): Promise<string> { afterEach(() => {
const result = await fetch( process.chdir('..')
`https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/${file}/${file}.kcl` })
)
const text = await result.text()
return text
}
async function getFileNamesFromManifestJSON(): Promise<KclSampleFile[]> { // The tests have to be sequential because we need to change directories
const result = await fetch( // to support `import` working properly.
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/refs/heads/main/manifest.json' // @ts-expect-error
) describe.sequential('Test KCL Samples from public Github repository', () => {
const json = await result.json() // @ts-expect-error
json.forEach((file: KclSampleFile) => { describe.sequential('when performing enginelessExecutor', () => {
const filenameWithoutExtension = file.file.split('.')[0] manifest.forEach((file: KclSampleFile) => {
file.filename = filenameWithoutExtension // @ts-expect-error
}) it.sequential(
return json `should execute ${file.title} (${file.file}) successfully`,
} async () => {
const [dirProject, fileKcl] =
// Value to use across all tests! file.pathFromProjectDirectoryToFirstFile.split('/')
let files: KclSampleFile[] = [] process.chdir(dirProject)
const code = await fs.readFile(fileKcl, 'utf-8')
describe('Test KCL Samples from public Github repository', () => {
describe('When parsing source code', () => {
// THIS RUNS ACROSS OTHER TESTS!
it('should fetch files', async () => {
files = await getFileNamesFromManifestJSON()
})
// Run through all of the files in the manifest json. This will allow us to be automatically updated
// with the latest changes in github. We won't be hard coding the filenames
files.forEach((file: KclSampleFile) => {
it(`should parse ${file.filename} without errors`, async () => {
const code = await getKclSampleCodeFromGithub(file.filename)
assertParse(code)
}, 1000)
})
})
describe('when performing enginelessExecutor', () => {
it(
'should run through all the files',
async () => {
for (let i = 0; i < files.length; i++) {
const file: KclSampleFile = files[i]
const code = await getKclSampleCodeFromGithub(file.filename)
const ast = assertParse(code) const ast = assertParse(code)
await enginelessExecutor(ast, programMemoryInit()) await enginelessExecutor(ast, programMemoryInit())
} },
}, files.length * 1000
files.length * 1000 )
) })
}) })
}) })

View File

@ -1,5 +1,21 @@
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
// Polyfill window.electron fs functions as needed when in a nodejs context
// (INTENDED FOR VITEST SHINANGANS.)
if (process.env.NODE_ENV === 'test' && process.env.VITEST) {
const fs = require('node:fs/promises')
const path = require('node:path')
Object.assign(window, {
electron: {
readFile: fs.readFile,
stat: fs.stat,
readdir: fs.readdir,
path,
process: {},
},
})
}
/// FileSystemManager is a class that provides a way to read files from the local file system. /// FileSystemManager is a class that provides a way to read files from the local file system.
/// It assumes that you are in a project since it is solely used by the std lib /// It assumes that you are in a project since it is solely used by the std lib
/// when executing code. /// when executing code.
@ -19,13 +35,9 @@ class FileSystemManager {
} }
async readFile(path: string): Promise<Uint8Array> { async readFile(path: string): Promise<Uint8Array> {
// Using local file system only works from desktop. // Using local file system only works from desktop and nodejs
if (!isDesktop()) { if (!window?.electron?.readFile) {
return Promise.reject( return Promise.reject(new Error('No polyfill found for this function'))
new Error(
'This function can only be called from the desktop application'
)
)
} }
return this.join(this.dir, path).then((filePath) => { return this.join(this.dir, path).then((filePath) => {
@ -35,12 +47,8 @@ class FileSystemManager {
async exists(path: string): Promise<boolean | void> { async exists(path: string): Promise<boolean | void> {
// Using local file system only works from desktop. // Using local file system only works from desktop.
if (!isDesktop()) { if (!window?.electron?.stat) {
return Promise.reject( return Promise.reject(new Error('No polyfill found for this function'))
new Error(
'This function can only be called from the desktop application'
)
)
} }
return this.join(this.dir, path).then(async (file) => { return this.join(this.dir, path).then(async (file) => {
@ -57,12 +65,8 @@ class FileSystemManager {
async getAllFiles(path: string): Promise<string[] | void> { async getAllFiles(path: string): Promise<string[] | void> {
// Using local file system only works from desktop. // Using local file system only works from desktop.
if (!isDesktop()) { if (!window?.electron?.readdir) {
return Promise.reject( return Promise.reject(new Error('No polyfill found for this function'))
new Error(
'This function can only be called from the desktop application'
)
)
} }
return this.join(this.dir, path).then((filepath) => { return this.join(this.dir, path).then((filepath) => {

View File

@ -3,6 +3,8 @@ import { isDesktop } from './isDesktop'
export type KclSamplesManifestItem = { export type KclSamplesManifestItem = {
file: string file: string
pathFromProjectDirectoryToFirstFile: string
multipleFiles: boolean
title: string title: string
description: string description: string
} }

View File

@ -49,20 +49,30 @@ export function kclCommands(
if (!data?.sample) { if (!data?.sample) {
return return
} }
const pathParts = data.sample.split('/')
const projectPathPart = pathParts[0]
const primaryKclFile = pathParts[1]
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent( const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
data.sample.replace(FILE_EXT, '') projectPathPart
)}/${encodeURIComponent(data.sample)}` )}/${encodeURIComponent(primaryKclFile)}`
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent( const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
data.sample.replace(FILE_EXT, '') projectPathPart
)}/${PROJECT_SETTINGS_FILE_NAME}` )}/${PROJECT_SETTINGS_FILE_NAME}`
Promise.all([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)]) Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)])
.then((results) => {
const a =
'value' in results[0] ? results[0].value : results[0].reason
const b =
'value' in results[1] ? results[1].value : results[1].reason
return [a, b]
})
.then( .then(
async ([ async ([
codeResponse, codeResponse,
settingsResponse, settingsResponse,
]): Promise<OnSubmitProps> => { ]): Promise<OnSubmitProps> => {
if (!(codeResponse.ok && settingsResponse.ok)) { if (!codeResponse.ok) {
console.error( console.error(
'Failed to fetch sample code:', 'Failed to fetch sample code:',
codeResponse.statusText codeResponse.statusText
@ -70,20 +80,24 @@ export function kclCommands(
return Promise.reject(new Error('Failed to fetch sample code')) return Promise.reject(new Error('Failed to fetch sample code'))
} }
const code = await codeResponse.text() const code = await codeResponse.text()
const parsedProjectSettings = parseProjectSettings(
await settingsResponse.text() // It's possible that a sample doesn't have a project.toml
) // associated with it.
let projectSettingsPayload: ReturnType< let projectSettingsPayload: ReturnType<
typeof projectConfigurationToSettingsPayload typeof projectConfigurationToSettingsPayload
> = {} > = {}
if (!err(parsedProjectSettings)) { if (settingsResponse.ok) {
projectSettingsPayload = projectConfigurationToSettingsPayload( const parsedProjectSettings = parseProjectSettings(
parsedProjectSettings await settingsResponse.text()
) )
if (!err(parsedProjectSettings)) {
projectSettingsPayload =
projectConfigurationToSettingsPayload(parsedProjectSettings)
}
} }
return { return {
sampleName: data.sample, sampleName: data.sample.split('/')[0] + FILE_EXT,
code, code,
method: data.method, method: data.method,
sampleUnits: sampleUnits:

View File

@ -19,6 +19,10 @@ const stdLibMap: Record<string, StdLibCallInfo> = {
label: 'Fillet', label: 'Fillet',
icon: 'fillet3d', icon: 'fillet3d',
}, },
helix: {
label: 'Helix',
icon: 'helix',
},
hole: { hole: {
label: 'Hole', label: 'Hole',
icon: 'hole', icon: 'hole',

View File

@ -207,6 +207,15 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
description: 'Create a hole in a 3D solid.', description: 'Create a hole in a 3D solid.',
links: [], links: [],
}, },
{
id: 'helix',
onClick: () => console.error('Helix not yet implemented'),
icon: 'helix',
status: 'kcl-only',
title: 'Helix',
description: 'Create a helix or spiral in 3D about an axis.',
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/helix' }],
},
'break', 'break',
[ [
{ {
@ -437,18 +446,19 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
icon: 'circle', icon: 'circle',
status: 'available', status: 'available',
title: 'Center circle', title: 'Center circle',
disabled: (state) => state.matches('Sketch no face'), disabled: (state) =>
isActive: (state) => state.matches({ Sketch: 'Circle tool' }), state.matches('Sketch no face') ||
(!canRectangleOrCircleTool(state.context) &&
!state.matches({ Sketch: 'Circle tool' }) &&
!state.matches({ Sketch: 'circle3PointToolSelect' })),
isActive: (state) =>
state.matches({ Sketch: 'Circle tool' }) ||
state.matches({ Sketch: 'circle3PointToolSelect' }),
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C', state.matches({ Sketch: 'Circle tool' }) ? ['Esc', 'C'] : 'C',
showTitle: false, showTitle: false,
description: 'Start drawing a circle from its center', description: 'Start drawing a circle from its center',
links: [ links: [],
{
label: 'GitHub issue',
url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
},
],
}, },
{ {
id: 'circle-three-points', id: 'circle-three-points',
@ -465,7 +475,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}), }),
icon: 'circle', icon: 'circle',
status: 'available', status: 'available',
title: 'Three-point circle', title: '3-point circle',
showTitle: false, showTitle: false,
description: 'Draw a circle defined by three points', description: 'Draw a circle defined by three points',
links: [], links: [],

View File

@ -432,6 +432,8 @@ export const modelingMachine = setup({
'has valid selection for deletion': () => false, 'has valid selection for deletion': () => false,
'is editing existing sketch': ({ context: { sketchDetails } }) => 'is editing existing sketch': ({ context: { sketchDetails } }) =>
isEditingExistingSketch({ sketchDetails }), isEditingExistingSketch({ sketchDetails }),
'is editing 3-point circle': ({ context: { sketchDetails } }) =>
isEditing3PointCircle({ sketchDetails }),
'Can make selection horizontal': ({ context: { selectionRanges } }) => { 'Can make selection horizontal': ({ context: { selectionRanges } }) => {
const info = horzVertInfo(selectionRanges, 'horizontal') const info = horzVertInfo(selectionRanges, 'horizontal')
if (trap(info)) return false if (trap(info)) return false
@ -2267,6 +2269,10 @@ export const modelingMachine = setup({
target: 'SketchIdle', target: 'SketchIdle',
guard: 'is editing existing sketch', guard: 'is editing existing sketch',
}, },
{
target: 'circle3PointToolSelect',
guard: 'is editing 3-point circle',
},
'Line tool', 'Line tool',
], ],
}, },
@ -2751,13 +2757,8 @@ export const modelingMachine = setup({
circle3PointToolSelect: { circle3PointToolSelect: {
invoke: { invoke: {
id: 'actor-circle-3-point', id: 'actor-circle-3-point',
input: function ({ context, event }) { input: function ({ context }) {
// These are not really necessary but I believe they are needed
// to satisfy TypeScript type narrowing or undefined check.
if (event.type !== 'change tool') return
if (event.data?.tool !== 'circle3Points') return
if (!context.sketchDetails) return if (!context.sketchDetails) return
return context.sketchDetails return context.sketchDetails
}, },
src: 'actorCircle3Point', src: 'actorCircle3Point',
@ -3025,6 +3026,34 @@ export function isEditingExistingSketch({
) )
return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle return (hasStartProfileAt && maybePipeExpression.body.length > 1) || hasCircle
} }
export function isEditing3PointCircle({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
if (!sketchDetails?.sketchPathToNode) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchPathToNode,
'VariableDeclarator'
)
if (err(variableDeclaration)) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasStartProfileAt = pipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
)
const hasCircle3Point = pipeExpression.body.some(
(item) =>
item.type === 'CallExpressionKw' &&
item.callee.name === 'circleThreePoint'
)
return (
(hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle3Point
)
}
export function pipeHasCircle({ export function pipeHasCircle({
sketchDetails, sketchDetails,
}: { }: {
@ -3045,3 +3074,62 @@ export function pipeHasCircle({
) )
return hasCircle return hasCircle
} }
export function pipeHasCircleThreePoint({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
if (!sketchDetails?.sketchPathToNode) return false
const variableDeclaration = getNodeFromPath<VariableDeclarator>(
kclManager.ast,
sketchDetails.sketchPathToNode,
'VariableDeclarator'
)
if (err(variableDeclaration)) return false
if (variableDeclaration.node.type !== 'VariableDeclarator') return false
const pipeExpression = variableDeclaration.node.init
if (pipeExpression.type !== 'PipeExpression') return false
const hasCircle = pipeExpression.body.some(
(item) =>
item.type === 'CallExpression' && item.callee.name === 'circleThreePoint'
)
return hasCircle
}
export function canRectangleOrCircleTool({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
sketchDetails?.sketchPathToNode || [],
'VariableDeclaration'
)
// This should not be returning false, and it should be caught
// but we need to simulate old behavior to move on.
if (err(node)) return false
return node.node?.declaration.init.type !== 'PipeExpression'
}
/** If the sketch contains `close` or `circle` stdlib functions it must be closed */
export function isClosedSketch({
sketchDetails,
}: {
sketchDetails: SketchDetails | null
}): boolean {
const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast,
sketchDetails?.sketchPathToNode || [],
'VariableDeclaration'
)
// This should not be returning false, and it should be caught
// but we need to simulate old behavior to move on.
if (err(node)) return false
if (node.node?.declaration?.init?.type !== 'PipeExpression') return false
return node.node.declaration.init.body.some(
(node) =>
node.type === 'CallExpression' &&
(node.callee.name === 'close' || node.callee.name === 'circle')
)
}

View File

@ -21,8 +21,6 @@ use crate::{
use super::cad_op::{OpArg, Operation}; use super::cad_op::{OpArg, Operation};
const FLOAT_TO_INT_MAX_DELTA: f64 = 0.01;
impl BinaryPart { impl BinaryPart {
#[async_recursion] #[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
@ -974,10 +972,9 @@ fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -
if num < 0.0 { if num < 0.0 {
return make_err(format!("'{num}' is negative, so you can't index an array with it")) return make_err(format!("'{num}' is negative, so you can't index an array with it"))
} }
let nearest_int = num.round(); let nearest_int = crate::try_f64_to_usize(num);
let delta = num-nearest_int; if let Some(nearest_int) = nearest_int {
if delta < FLOAT_TO_INT_MAX_DELTA { Ok(Property::UInt(nearest_int))
Ok(Property::UInt(nearest_int as usize))
} else { } else {
make_err(format!("'{num}' is not an integer, so you can't index an array with it")) make_err(format!("'{num}' is not an integer, so you can't index an array with it"))
} }
@ -988,6 +985,7 @@ fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -
} }
} }
} }
impl Property { impl Property {
fn type_name(&self) -> &'static str { fn type_name(&self) -> &'static str {
match self { match self {

View File

@ -70,7 +70,7 @@ mod settings;
#[cfg(test)] #[cfg(test)]
mod simulation_tests; mod simulation_tests;
mod source_range; mod source_range;
mod std; pub mod std;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub mod test_server; pub mod test_server;
mod thread; mod thread;
@ -84,7 +84,7 @@ pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs}; pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
pub use execution::{ pub use execution::{
cache::{CacheInformation, OldAstState}, cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings, ExecState, ExecutorContext, ExecutorSettings, Point2d,
}; };
pub use lsp::{ pub use lsp::{
copilot::Backend as CopilotLspBackend, copilot::Backend as CopilotLspBackend,

View File

@ -30,7 +30,6 @@ use crate::{
token::{Token, TokenSlice, TokenType}, token::{Token, TokenSlice, TokenType},
PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR, PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR,
}, },
unparser::ExprContext,
SourceRange, SourceRange,
}; };
@ -2611,23 +2610,6 @@ fn fn_call(i: &mut TokenSlice) -> PResult<Node<CallExpression>> {
} }
let end = preceded(opt(whitespace), close_paren).parse_next(i)?.end; let end = preceded(opt(whitespace), close_paren).parse_next(i)?.end;
// This should really be done with resolved names, but we don't have warning support there
// so we'll hack this in here.
if fn_name.name == "int" {
assert_eq!(args.len(), 1);
let mut arg_str = args[0].recast(&crate::FormatOptions::default(), 0, ExprContext::Other);
if arg_str.contains('.') && !arg_str.ends_with(".0") {
arg_str = format!("round({arg_str})");
}
ParseContext::warn(CompilationError::with_suggestion(
SourceRange::new(fn_name.start, end, fn_name.module_id),
None,
"`int` function is deprecated. You may not need it at all. If you need to round, consider `round`, `ceil`, or `floor`.",
Some(("Remove call to `int`", arg_str)),
Tag::Deprecated,
));
}
Ok(Node { Ok(Node {
start: fn_name.start, start: fn_name.start,
end, end,
@ -4346,17 +4328,6 @@ sketch001 = startSketchOn('XZ') |> startProfileAt([90.45, 119.09, %)"#;
assert_eq!(errs[0].apply_suggestion(some_program_string).unwrap(), "{ foo = bar }") assert_eq!(errs[0].apply_suggestion(some_program_string).unwrap(), "{ foo = bar }")
} }
#[test]
fn warn_fn_int() {
let some_program_string = r#"int(1.0)
int(42.3)"#;
let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 2);
let replaced = errs[1].apply_suggestion(some_program_string).unwrap();
let replaced = errs[0].apply_suggestion(&replaced).unwrap();
assert_eq!(replaced, "1.0\nround(42.3)");
}
#[test] #[test]
fn warn_fn_decl() { fn warn_fn_decl() {
let some_program_string = r#"fn foo = () => { let some_program_string = r#"fn foo = () => {

View File

@ -55,6 +55,10 @@ impl KwArgs {
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 } self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
} }
/// Are there no arguments?
pub fn is_empty(&self) -> bool {
self.labeled.len() == 0 && self.unlabeled.is_none()
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -34,6 +34,7 @@ pub async fn int(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
#[stdlib { #[stdlib {
name = "int", name = "int",
tags = ["convert"], tags = ["convert"],
deprecated = true,
}] }]
fn inner_int(num: f64) -> Result<f64, KclError> { fn inner_int(num: f64) -> Result<f64, KclError> {
Ok(num) Ok(num)

View File

@ -270,6 +270,19 @@ pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64
[x, y] [x, y]
} }
pub struct CircleParams {
pub center: Point2d,
pub radius: f64,
}
pub fn calculate_circle_from_3_points(points: [Point2d; 3]) -> CircleParams {
let center: Point2d = calculate_circle_center(points[0].into(), points[1].into(), points[2].into()).into();
CircleParams {
center,
radius: distance(center, points[1]),
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
// Here you can bring your functions into scope // Here you can bring your functions into scope

View File

@ -5,7 +5,7 @@ use std::sync::Arc;
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use gloo_utils::format::JsValueSerdeExt; use gloo_utils::format::JsValueSerdeExt;
use kcl_lib::{ use kcl_lib::{
exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Program, exec::IdGenerator, CacheInformation, CoreDump, EngineManager, ExecState, ModuleId, OldAstState, Point2d, Program,
}; };
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tower_lsp::{LspService, Server}; use tower_lsp::{LspService, Server};
@ -576,3 +576,26 @@ pub fn base64_decode(input: &str) -> Result<Vec<u8>, JsValue> {
Err(JsValue::from_str("Invalid base64 encoding")) Err(JsValue::from_str("Invalid base64 encoding"))
} }
#[wasm_bindgen]
pub struct WasmCircleParams {
pub center_x: f64,
pub center_y: f64,
pub radius: f64,
}
/// Calculate a circle from 3 points.
#[wasm_bindgen]
pub fn calculate_circle_from_3_points(ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64) -> WasmCircleParams {
let result = kcl_lib::std::utils::calculate_circle_from_3_points([
Point2d { x: ax, y: ay },
Point2d { x: bx, y: by },
Point2d { x: cx, y: cy },
]);
WasmCircleParams {
center_x: result.center.x,
center_y: result.center.y,
radius: result.radius,
}
}