move kcl.py to this repo (#5587)
* ci for kcl-python-bindings Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * bettter concurrency Signed-off-by: Jess Frazelle <github@jessfraz.com> * cleanup files Signed-off-by: Jess Frazelle <github@jessfraz.com> * updates Signed-off-by: Jess Frazelle <github@jessfraz.com> * updaets Signed-off-by: Jess Frazelle <github@jessfraz.com> * updaets Signed-off-by: Jess Frazelle <github@jessfraz.com> * updaets Signed-off-by: Jess Frazelle <github@jessfraz.com> * updaets Signed-off-by: Jess Frazelle <github@jessfraz.com> * format Signed-off-by: Jess Frazelle <github@jessfraz.com> * format Signed-off-by: Jess Frazelle <github@jessfraz.com> --------- Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@ -25,7 +25,7 @@ updates:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
directory: '/rust/' # Location of package manifests
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
@ -39,3 +39,11 @@ updates:
|
||||
wasm-bindgen-deps:
|
||||
patterns:
|
||||
- "wasm-bindgen*"
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/rust/kcl-python-bindings/" # Location of package manifests
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: monday
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
|
179
.github/workflows/kcl-python-bindings.yml
vendored
Normal file
179
.github/workflows/kcl-python-bindings.yml
vendored
Normal file
@ -0,0 +1,179 @@
|
||||
# This file is autogenerated by maturin v1.6.0 and then modified
|
||||
# To update, run
|
||||
#
|
||||
# maturin generate-ci github
|
||||
#
|
||||
name: kcl-python-bindings
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- 'rust/kcl-python-bindings/**'
|
||||
- '**.rs'
|
||||
- .github/workflows/kcl-python-bindings.yml
|
||||
tags:
|
||||
- 'kcl-*'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- 'rust/kcl-python-bindings/**'
|
||||
- '**.rs'
|
||||
- .github/workflows/kcl-python-bindings.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux-x86_64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
working-directory: rust/kcl-python-bindings
|
||||
target: x86_64
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
manylinux: auto
|
||||
before-script-linux: |
|
||||
yum install openssl-devel -y
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-linux-x86_64
|
||||
path: rust/kcl-python-bindings/dist
|
||||
|
||||
windows:
|
||||
runs-on: windows-16-cores
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- x64
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
architecture: ${{ matrix.target }}
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
working-directory: rust/kcl-python-bindings
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-windows-${{ matrix.target }}
|
||||
path: rust/kcl-python-bindings/dist
|
||||
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target:
|
||||
- x86_64
|
||||
- aarch64
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.x
|
||||
- name: Build wheels
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
working-directory: rust/kcl-python-bindings
|
||||
target: ${{ matrix.target }}
|
||||
args: --release --out dist --find-interpreter
|
||||
sccache: 'true'
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-macos-${{ matrix.target }}
|
||||
path: rust/kcl-python-bindings/dist
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- uses: taiki-e/install-action@just
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd rust/kcl-python-bindings
|
||||
just setup-uv
|
||||
just test
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
|
||||
sdist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- name: Install codespell
|
||||
run: |
|
||||
uv venv .venv
|
||||
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
|
||||
echo "$PWD/.venv/bin" >> $GITHUB_PATH
|
||||
uv pip install pip --upgrade
|
||||
- name: Build sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
working-directory: rust/kcl-python-bindings
|
||||
command: sdist
|
||||
args: --out dist
|
||||
- name: Upload sdist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wheels-sdist
|
||||
path: rust/kcl-python-bindings/dist
|
||||
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: [linux-x86_64, windows, macos, sdist]
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- name: Install codespell
|
||||
run: |
|
||||
cd rust/kcl-python-bindings
|
||||
uv venv .venv
|
||||
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
|
||||
echo "$PWD/.venv/bin" >> $GITHUB_PATH
|
||||
uv pip install pip --upgrade
|
||||
- name: Publish to PyPI
|
||||
uses: PyO3/maturin-action@v1
|
||||
env:
|
||||
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||
with:
|
||||
working-directory: rust/kcl-python-bindings
|
||||
command: upload
|
||||
args: --non-interactive --skip-existing wheels-*/*
|
24
.github/workflows/ruff.yml
vendored
Normal file
24
.github/workflows/ruff.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: ruff
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
paths:
|
||||
- '**.py'
|
||||
- .github/workflows/ruff.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.py'
|
||||
- .github/workflows/ruff.yml
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -63,8 +63,13 @@ Mac_App_Distribution.provisionprofile
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
venv
|
||||
.vite/
|
||||
|
||||
# electron
|
||||
out/
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
uv.lock
|
||||
rust/kcl-python-bindings/dist
|
||||
venv
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
20
rust/Cargo.lock
generated
20
rust/Cargo.lock
generated
@ -1744,6 +1744,21 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-python-bindings"
|
||||
version = "0.3.45"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kcl-lib",
|
||||
"kittycad-modeling-cmds",
|
||||
"miette",
|
||||
"pyo3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.45"
|
||||
@ -1833,9 +1848,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kittycad-modeling-cmds"
|
||||
version = "0.2.99"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "828a0c74476533e6258ea7dd70cfc7d63a5df4b37753d30ef198e0689eaac4eb"
|
||||
checksum = "fb5a824cb9bb4c602962ecbaca5ce71225938aa1abc24103bf46c222f468dd26"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -2530,6 +2545,7 @@ dependencies = [
|
||||
"pyo3-build-config",
|
||||
"pyo3-ffi",
|
||||
"pyo3-macros",
|
||||
"serde",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
|
@ -3,6 +3,7 @@ resolver = "2"
|
||||
members = [
|
||||
"kcl-derive-docs",
|
||||
"kcl-lib",
|
||||
"kcl-python-bindings",
|
||||
"kcl-test-server",
|
||||
"kcl-to-core",
|
||||
"kcl-wasm-lib"
|
||||
@ -23,10 +24,14 @@ similar = { opt-level = 3 }
|
||||
debug = "line-tables-only"
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.85"
|
||||
anyhow = { version = "1" }
|
||||
http = "1"
|
||||
indexmap = "2.7.0"
|
||||
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
|
||||
kittycad-modeling-cmds = { version = "0.2.99", features = ["ts-rs", "websocket"] }
|
||||
kittycad-modeling-cmds = { version = "0.2.100", features = ["ts-rs", "websocket"] }
|
||||
miette = "7.5.0"
|
||||
pyo3 = { version = "0.22.6" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1" }
|
||||
tokio = { version = "1" }
|
||||
|
@ -46,7 +46,9 @@ test:
|
||||
|
||||
publish-kcl version:
|
||||
git tag kcl-{{version}}
|
||||
git push origin kcl-{{version}}
|
||||
cargo publish -p kcl-derive-docs
|
||||
cargo publish -p kcl-lib
|
||||
cargo publish -p kcl-test-server
|
||||
# We push the tag at the end of publish since pushing the tag
|
||||
# will trigger CI to release the kcl python bindings.
|
||||
git push origin kcl-{{version}}
|
||||
|
@ -23,7 +23,7 @@ serde_tokenstream = "0.2"
|
||||
syn = { version = "2.0.96", features = ["full"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.95"
|
||||
anyhow = { workspace = true }
|
||||
expectorate = "1.1.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
rustfmt-wrapper = "0.2.1"
|
||||
|
@ -11,9 +11,9 @@ keywords = ["kcl", "KittyCAD", "CAD"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.95", features = ["backtrace"] }
|
||||
anyhow = { workspace = true, features = ["backtrace"] }
|
||||
async-recursion = "1.1.1"
|
||||
async-trait = "0.1.85"
|
||||
async-trait = {workspace = true}
|
||||
base64 = "0.22.1"
|
||||
chrono = "0.4.38"
|
||||
clap = { version = "4.5.27", default-features = false, optional = true, features = [
|
||||
@ -37,10 +37,10 @@ kittycad = { workspace = true }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
lazy_static = "1.5.0"
|
||||
measurements = "0.11.0"
|
||||
miette = "7.5.0"
|
||||
miette = { workspace = true }
|
||||
mime_guess = "2.0.5"
|
||||
parse-display = "0.9.1"
|
||||
pyo3 = { version = "0.22.6", optional = true }
|
||||
pyo3 = { workspace = true, optional = true }
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12", default-features = false, features = [
|
||||
"stream",
|
||||
|
@ -1,4 +1,4 @@
|
||||
# KCL
|
||||
# kcl-lib
|
||||
|
||||
Our language for defining geometry and working with our Geometry Engine efficiently. Short for KittyCAD Language, named after our Design API.
|
||||
|
||||
|
26
rust/kcl-python-bindings/Cargo.toml
Normal file
26
rust/kcl-python-bindings/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "kcl-python-bindings"
|
||||
version = "0.3.45"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/kittycad/modeling-app"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "kcl"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
kcl-lib = { path = "../kcl-lib", features = [
|
||||
"pyo3",
|
||||
"engine",
|
||||
"disable-println",
|
||||
] }
|
||||
#kcl-lib = { path = "../modeling-app/src/wasm-lib/kcl", default-features = false, features = ["pyo3", "engine", "disable-println"] }
|
||||
kittycad-modeling-cmds = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy"] }
|
||||
pyo3 = { workspace = true, features = ["serde", "experimental-async"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
39
rust/kcl-python-bindings/README.md
Normal file
39
rust/kcl-python-bindings/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# kcl-python-bindings
|
||||
|
||||
Python bindings to the rust kcl-lib crate.
|
||||
|
||||
## Usage
|
||||
|
||||
The [tests.py](tests/tests.py) file contains examples of how to use the library.
|
||||
|
||||
## Development
|
||||
|
||||
We use [maturin](https://github.com/PyO3/maturin) for this project.
|
||||
|
||||
You can either download binaries from the [latest release](https://github.com/PyO3/maturin/releases/latest) or install it with [pipx](https://pypa.github.io/pipx/):
|
||||
|
||||
```shell
|
||||
pipx install maturin
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> `pip install maturin` should also work if you don't want to use pipx.
|
||||
|
||||
There are four main commands:
|
||||
|
||||
- `maturin publish` builds the crate into python packages and publishes them to pypi.
|
||||
- `maturin build` builds the wheels and stores them in a folder (`target/wheels` by default), but doesn't upload them. It's possible to upload those with [twine](https://github.com/pypa/twine) or `maturin upload`.
|
||||
- `maturin develop` builds the crate and installs it as a python module directly in the current virtualenv. Note that while `maturin develop` is faster, it doesn't support all the feature that running `pip install` after `maturin build` supports.
|
||||
|
||||
`pyo3` bindings are automatically detected.
|
||||
`maturin` doesn't need extra configuration files and doesn't clash with an existing setuptools-rust or milksnake configuration.
|
||||
|
||||
### Releasing a new version
|
||||
|
||||
1. Make sure the `Cargo.toml` has the new version you want to release.
|
||||
2. Run `make tag` this is just an easy command for making a tag formatted
|
||||
correctly with the version.
|
||||
3. Push the tag (the result of `make tag` gives instructions for this)
|
||||
4. Everything else is triggered from the tag push. Just make sure all the tests
|
||||
pass on the `main` branch before making and pushing a new tag.
|
14
rust/kcl-python-bindings/files/box_with_linter_errors.kcl
Normal file
14
rust/kcl-python-bindings/files/box_with_linter_errors.kcl
Normal file
@ -0,0 +1,14 @@
|
||||
// A 25x25x50 box
|
||||
|
||||
const box_width = 25
|
||||
const box_depth = 25
|
||||
const box_height = 50
|
||||
|
||||
const box_sketch = startSketchOn('XY')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(box_width, %, $line1)
|
||||
|> yLine(box_depth, %, $line2)
|
||||
|> xLineTo(profileStartX(%), %, $line3)
|
||||
|> close(%, $line4)
|
||||
|
||||
const box3D = extrude(box_sketch, length = box_height)
|
1
rust/kcl-python-bindings/files/parse_file_error/main.kcl
Normal file
1
rust/kcl-python-bindings/files/parse_file_error/main.kcl
Normal file
@ -0,0 +1 @@
|
||||
import thing from 'thing.kcl'
|
@ -0,0 +1 @@
|
||||
lksjndflsskjfnak;jfna##
|
50
rust/kcl-python-bindings/files/walkie-talkie/antenna.kcl
Normal file
50
rust/kcl-python-bindings/files/walkie-talkie/antenna.kcl
Normal file
@ -0,0 +1,50 @@
|
||||
// Antenna
|
||||
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
|
||||
// import constants
|
||||
import height, width, antennaBaseWidth, antennaBaseHeight, antennaTopWidth, antennaTopHeight from "globals.kcl"
|
||||
|
||||
// Calculate the origin
|
||||
origin = [-width / 2 + .45, -0.10]
|
||||
|
||||
// Create the antenna
|
||||
antennaX = origin[0]
|
||||
antennaY = origin[1]
|
||||
|
||||
antennaPlane = {
|
||||
plane = {
|
||||
origin = { x = 0, y = 0, z = height / 2 },
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 1, z = 0 },
|
||||
zAxis = { x = 0, y = 0, z = 1 }
|
||||
}
|
||||
}
|
||||
|
||||
// Create the antenna base sketch
|
||||
sketch001 = startSketchOn(antennaPlane)
|
||||
|> startProfileAt([origin[0], origin[1]], %)
|
||||
|> line(end = [antennaBaseWidth, 0])
|
||||
|> line(end = [0, -antennaBaseHeight])
|
||||
|> line(end = [-antennaBaseWidth, 0])
|
||||
|> close()
|
||||
|
||||
// Create the antenna top sketch
|
||||
loftPlane = offsetPlane('XY', offset = height / 2 + 3)
|
||||
|
||||
sketch002 = startSketchOn(loftPlane)
|
||||
|> startProfileAt([
|
||||
origin[0] + (antennaBaseWidth - antennaTopWidth) / 2,
|
||||
origin[1] - ((antennaBaseHeight - antennaTopHeight) / 2)
|
||||
], %)
|
||||
|> xLine(antennaTopWidth, %)
|
||||
|> yLine(-antennaTopHeight, %)
|
||||
|> xLine(-antennaTopWidth, %)
|
||||
|> close()
|
||||
|
||||
// Create the antenna using a loft
|
||||
loft([sketch001, sketch002])
|
||||
|> appearance(color = "#000000")
|
80
rust/kcl-python-bindings/files/walkie-talkie/body.kcl
Normal file
80
rust/kcl-python-bindings/files/walkie-talkie/body.kcl
Normal file
@ -0,0 +1,80 @@
|
||||
// Walkie talkie body
|
||||
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
|
||||
// Import constants
|
||||
import height, width, thickness, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight from "globals.kcl"
|
||||
|
||||
bodySketch = startSketchOn('XZ')
|
||||
|> startProfileAt([-width / 2, height / 2], %)
|
||||
|> xLine(width, %, $chamfer1)
|
||||
|> yLine(-height, %, $chamfer2)
|
||||
|> xLine(-width, %, $chamfer3)
|
||||
|> close(tag = $chamfer4)
|
||||
bodyExtrude = extrude(bodySketch, length = thickness)
|
||||
|> chamfer(
|
||||
length = chamferLength,
|
||||
tags = [
|
||||
getNextAdjacentEdge(chamfer1),
|
||||
getNextAdjacentEdge(chamfer2),
|
||||
getNextAdjacentEdge(chamfer3),
|
||||
getNextAdjacentEdge(chamfer4)
|
||||
]
|
||||
)
|
||||
|
||||
// Define the offset for the indentation
|
||||
sketch002 = startSketchOn(bodyExtrude, 'END')
|
||||
|> startProfileAt([
|
||||
-width / 2 + offset,
|
||||
height / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))
|
||||
], %)
|
||||
|> angledLineToY({ angle = 45, to = height / 2 - offset }, %)
|
||||
|> line(endAbsolute = [
|
||||
width / 2 - (chamferLength + offset / 2 * cos(toRadians(45))),
|
||||
height / 2 - offset
|
||||
])
|
||||
|> angledLineToX({ angle = -45, to = width / 2 - offset }, %)
|
||||
|> line(endAbsolute = [
|
||||
width / 2 - offset,
|
||||
-(height / 2 - (chamferLength + offset / 2 * cos(toRadians(45))))
|
||||
])
|
||||
|> angledLineToY({
|
||||
angle = -135,
|
||||
to = -height / 2 + offset
|
||||
}, %)
|
||||
|> line(endAbsolute = [
|
||||
-(width / 2 - (chamferLength + offset / 2 * cos(toRadians(45)))),
|
||||
-height / 2 + offset
|
||||
])
|
||||
|> angledLineToX({
|
||||
angle = -225,
|
||||
to = -width / 2 + offset
|
||||
}, %)
|
||||
|> close()
|
||||
extrude002 = extrude(sketch002, length = -0.0625)
|
||||
|
||||
// Create the pocket for the screen
|
||||
sketch003 = startSketchOn(extrude002, 'start')
|
||||
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|
||||
|> xLine(screenWidth, %, $seg01)
|
||||
|> yLine(-screenHeight, %)
|
||||
|> xLine(-segLen(seg01), %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude003 = extrude(sketch003, length = screenDepth)
|
||||
|
||||
// Create the speaker box
|
||||
sketch004 = startSketchOn(extrude002, 'start')
|
||||
|> startProfileAt([-1.25 / 2, -.125], %)
|
||||
|> xLine(speakerBoxWidth, %)
|
||||
|> yLine(-speakerBoxHeight, %)
|
||||
|> xLine(-speakerBoxWidth, %)
|
||||
|> close()
|
||||
extrude(sketch004, length = -.5)
|
||||
|> appearance(
|
||||
color = "#277bb0",
|
||||
)
|
||||
|
38
rust/kcl-python-bindings/files/walkie-talkie/button.kcl
Normal file
38
rust/kcl-python-bindings/files/walkie-talkie/button.kcl
Normal file
@ -0,0 +1,38 @@
|
||||
// Walkie Talkie button
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
// Import constants
|
||||
import screenHeight, buttonWidth, tolerance, buttonHeight, buttonThickness from 'globals.kcl'
|
||||
|
||||
|
||||
// Create a function for the button
|
||||
export fn button(origin, rotation, plane) {
|
||||
buttonSketch = startSketchOn(plane)
|
||||
|> startProfileAt([origin[0], origin[1]], %)
|
||||
|> angledLine({
|
||||
angle = 180 + rotation,
|
||||
length = buttonWidth
|
||||
}, %, $tag1)
|
||||
|> angledLine({
|
||||
angle = 270 + rotation,
|
||||
length = buttonHeight
|
||||
}, %, $tag2)
|
||||
|> angledLine({
|
||||
angle = 0 + rotation,
|
||||
length = buttonWidth
|
||||
}, %)
|
||||
|> close()
|
||||
buttonExtrude = extrude(buttonSketch, length = buttonThickness)
|
||||
|> chamfer(
|
||||
length = .050,
|
||||
tags = [
|
||||
getNextAdjacentEdge(tag1),
|
||||
getNextAdjacentEdge(tag2)
|
||||
]
|
||||
)
|
||||
|> appearance(color = "#ff0000")
|
||||
|
||||
return buttonExtrude
|
||||
}
|
85
rust/kcl-python-bindings/files/walkie-talkie/case.kcl
Normal file
85
rust/kcl-python-bindings/files/walkie-talkie/case.kcl
Normal file
@ -0,0 +1,85 @@
|
||||
// Walkie talkie case
|
||||
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
|
||||
// Import constants and Zoo logo
|
||||
import width, height, chamferLength, offset, screenWidth, screenHeight, screenYPosition, screenDepth, speakerBoxWidth, speakerBoxHeight, squareHoleSideLength, caseTolerance from "globals.kcl"
|
||||
import zLogo, oLogo, oLogo2 from "zoo-logo.kcl"
|
||||
|
||||
plane = offsetPlane("XZ", offset = 1)
|
||||
|
||||
fn screenHole(sketchStart) {
|
||||
sketch006 = startSketchOn(sketchStart)
|
||||
|> startProfileAt([-screenWidth / 2, screenYPosition], %)
|
||||
|> xLine(screenWidth, %)
|
||||
|> yLine(-screenHeight, %)
|
||||
|> xLine(-screenWidth, %)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
return sketch006
|
||||
}
|
||||
|
||||
fn squareHolePattern(plane, x, y) {
|
||||
fn transformX(i) {
|
||||
return { translate = [.125 * i, 0] }
|
||||
}
|
||||
fn transformY(i) {
|
||||
return { translate = [0, -.125 * i] }
|
||||
}
|
||||
squareHolePatternSketch = startSketchOn(plane)
|
||||
|> startProfileAt([-x, -y], %)
|
||||
|> line(end = [squareHoleSideLength / 2, 0])
|
||||
|> line(end = [0, -squareHoleSideLength / 2])
|
||||
|> line(end = [-squareHoleSideLength / 2, 0])
|
||||
|> close()
|
||||
|> patternTransform2d(instances = 13, transform = transformX)
|
||||
|> patternTransform2d(instances = 11, transform = transformY)
|
||||
return squareHolePatternSketch
|
||||
}
|
||||
sketch005 = startSketchOn(offsetPlane("XZ", offset = 1))
|
||||
|> startProfileAt([
|
||||
-width / 2 + offset + caseTolerance,
|
||||
height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))
|
||||
], %)
|
||||
|> angledLineToY({
|
||||
angle = 45,
|
||||
to = height / 2 - (offset + caseTolerance)
|
||||
}, %)
|
||||
|> line(endAbsolute = [
|
||||
width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))),
|
||||
height / 2 - (offset + caseTolerance)
|
||||
])
|
||||
|> angledLineToX({
|
||||
angle = -45,
|
||||
to = width / 2 - (offset + caseTolerance)
|
||||
}, %)
|
||||
|> line(endAbsolute = [
|
||||
width / 2 - (offset + caseTolerance),
|
||||
-(height / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45))))
|
||||
])
|
||||
|> angledLineToY({
|
||||
angle = -135,
|
||||
to = -height / 2 + offset + caseTolerance
|
||||
}, %)
|
||||
|> line(endAbsolute = [
|
||||
-(width / 2 - (chamferLength + (offset + caseTolerance) / 2 * cos(toRadians(45)))),
|
||||
-height / 2 + offset + caseTolerance
|
||||
])
|
||||
|> angledLineToX({
|
||||
angle = -225,
|
||||
to = -width / 2 + offset + caseTolerance
|
||||
}, %)
|
||||
|> close()
|
||||
|> hole(screenHole(plane), %)
|
||||
|> hole(squareHolePattern(plane, .75, .125), %)
|
||||
|> hole(zLogo(plane, [-.30, -1.825], .20), %)
|
||||
|> hole(oLogo(plane, [-.075, -1.825], .20), %)
|
||||
|> hole(oLogo2(plane, [-.075, -1.825], .20), %)
|
||||
|> hole(oLogo(plane, [.175, -1.825], .20), %)
|
||||
|> hole(oLogo2(plane, [.175, -1.825], .20), %)
|
||||
|
||||
extrude(sketch005, length = -0.0625)
|
||||
|> appearance(color = '#D0FF01', metalness = 0, roughness = 50)
|
42
rust/kcl-python-bindings/files/walkie-talkie/globals.kcl
Normal file
42
rust/kcl-python-bindings/files/walkie-talkie/globals.kcl
Normal file
@ -0,0 +1,42 @@
|
||||
// Global constants for the walkie talkie
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
// body
|
||||
export height = 4
|
||||
export width = 2.5
|
||||
export thickness = 1
|
||||
export chamferLength = .325
|
||||
export offset = .125
|
||||
export screenWidth = 1.75
|
||||
export screenHeight = 1
|
||||
export screenYPosition = height / 2 - 0.75
|
||||
export screenDepth = -.0625
|
||||
export speakerBoxWidth = 1.25
|
||||
export speakerBoxHeight = 1.25
|
||||
|
||||
// antenna
|
||||
export antennaBaseWidth = .5
|
||||
export antennaBaseHeight = .25
|
||||
export antennaTopWidth = .30
|
||||
export antennaTopHeight = .05
|
||||
|
||||
// button
|
||||
export buttonWidth = .15
|
||||
export tolerance = 0.020
|
||||
export buttonHeight = screenHeight / 2 - tolerance
|
||||
export buttonThickness = .040
|
||||
|
||||
// case
|
||||
export squareHoleSideLength = 0.0625
|
||||
export caseTolerance = 0.010
|
||||
|
||||
// knob
|
||||
export knobDiameter = .5
|
||||
export knobHeight = .25
|
||||
export knobRadius = 0.050
|
||||
|
||||
// talk-button
|
||||
export talkButtonSideLength = 0.5
|
||||
export talkButtonHeight = 0.050
|
38
rust/kcl-python-bindings/files/walkie-talkie/knob.kcl
Normal file
38
rust/kcl-python-bindings/files/walkie-talkie/knob.kcl
Normal file
@ -0,0 +1,38 @@
|
||||
// Walkie talkie knob
|
||||
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
|
||||
// Import constants
|
||||
import width, thickness, height, knobDiameter, knobHeight, knobRadius from "globals.kcl"
|
||||
|
||||
// Define the plane for the knob
|
||||
knobPlane = {
|
||||
plane = {
|
||||
origin = {
|
||||
x = width / 2 - 0.70,
|
||||
y = -thickness / 2,
|
||||
z = height / 2
|
||||
},
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
zAxis = { x = 0, y = 1, z = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Create the knob sketch and revolve
|
||||
startSketchOn(knobPlane)
|
||||
|> startProfileAt([0.0001, 0], %)
|
||||
|> xLine(knobDiameter / 2, %)
|
||||
|> yLine(knobHeight - 0.05, %)
|
||||
|> arc({
|
||||
angleStart = 0,
|
||||
angleEnd = 90,
|
||||
radius = .05
|
||||
}, %)
|
||||
|> xLineTo(0.0001, %)
|
||||
|> close()
|
||||
|> revolve({ axis = "Y" }, %)
|
||||
|> appearance(color = '#D0FF01', metalness = 90, roughness = 50)
|
50
rust/kcl-python-bindings/files/walkie-talkie/main.kcl
Normal file
50
rust/kcl-python-bindings/files/walkie-talkie/main.kcl
Normal file
@ -0,0 +1,50 @@
|
||||
// Walkie Talkie
|
||||
// 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.
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
// Import parts and constants
|
||||
import 'body.kcl'
|
||||
import 'antenna.kcl'
|
||||
import 'case.kcl'
|
||||
import 'talk-button.kcl' as talkButton
|
||||
import 'knob.kcl'
|
||||
import button from "button.kcl"
|
||||
import width, height, thickness, screenWidth, screenHeight, screenYPosition, tolerance from "globals.kcl"
|
||||
|
||||
// Import the body
|
||||
body
|
||||
|
||||
// Import the case
|
||||
case
|
||||
|
||||
// Import the antenna
|
||||
antenna
|
||||
|
||||
// Import the buttons
|
||||
button([
|
||||
-(screenWidth / 2 + tolerance),
|
||||
screenYPosition
|
||||
], 0, offsetPlane("XZ", offset = thickness))
|
||||
button([
|
||||
-(screenWidth / 2 + tolerance),
|
||||
screenYPosition - (screenHeight / 2)
|
||||
], 0, offsetPlane("XZ", offset = thickness))
|
||||
button([
|
||||
screenWidth / 2 + tolerance,
|
||||
screenYPosition - screenHeight
|
||||
], 180, offsetPlane("XZ", offset = thickness))
|
||||
button([
|
||||
screenWidth / 2 + tolerance,
|
||||
screenYPosition - (screenHeight / 2)
|
||||
], 180, offsetPlane("XZ", offset = thickness))
|
||||
|
||||
// Import the talk button
|
||||
talkButton
|
||||
|
||||
// Import the frequency knob
|
||||
knob
|
||||
|
||||
|
||||
|
46
rust/kcl-python-bindings/files/walkie-talkie/talk-button.kcl
Normal file
46
rust/kcl-python-bindings/files/walkie-talkie/talk-button.kcl
Normal file
@ -0,0 +1,46 @@
|
||||
// Walkie talkie talk button
|
||||
|
||||
|
||||
// Set units
|
||||
@settings(defaultLengthUnit = in)
|
||||
|
||||
|
||||
// Import constants
|
||||
import width, thickness, talkButtonSideLength, talkButtonHeight from "globals.kcl"
|
||||
|
||||
talkButtonPlane = {
|
||||
plane = {
|
||||
origin = {
|
||||
x = width / 2,
|
||||
y = -thickness / 2,
|
||||
z = .5
|
||||
},
|
||||
xAxis = { x = 0, y = 1, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
zAxis = { x = 1, y = 0, z = 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Create the talk button sketch
|
||||
talkButtonSketch = startSketchOn(talkButtonPlane)
|
||||
|> startProfileAt([
|
||||
-talkButtonSideLength / 2,
|
||||
talkButtonSideLength / 2
|
||||
], %)
|
||||
|> xLine(talkButtonSideLength, %, $tag1)
|
||||
|> yLine(-talkButtonSideLength, %, $tag2)
|
||||
|> xLine(-talkButtonSideLength, %, $tag3)
|
||||
|> close(tag = $tag4)
|
||||
|
||||
// Create the talk button and apply fillets
|
||||
extrude(talkButtonSketch, length = talkButtonHeight)
|
||||
|> fillet(
|
||||
radius = 0.050,
|
||||
tags = [
|
||||
getNextAdjacentEdge(tag1),
|
||||
getNextAdjacentEdge(tag2),
|
||||
getNextAdjacentEdge(tag3),
|
||||
getNextAdjacentEdge(tag4)
|
||||
]
|
||||
)
|
||||
|> appearance(color = '#D0FF01', metalness = 90, roughness = 90)
|
83
rust/kcl-python-bindings/files/walkie-talkie/zoo-logo.kcl
Normal file
83
rust/kcl-python-bindings/files/walkie-talkie/zoo-logo.kcl
Normal file
@ -0,0 +1,83 @@
|
||||
// Zoo logo
|
||||
|
||||
// Define a function to draw the ZOO "Z"
|
||||
export fn zLogo(surface, origin, scale) {
|
||||
zSketch = surface
|
||||
|> startProfileAt([
|
||||
0 + origin[0],
|
||||
0.15 * scale + origin[1]
|
||||
], %)
|
||||
|> yLine(-0.15 * scale, %)
|
||||
|> xLine(0.15 * scale, %)
|
||||
|> angledLineToX({
|
||||
angle = 47.15,
|
||||
to = 0.3 * scale + origin[0]
|
||||
}, %, $seg1)
|
||||
|> yLineTo(0 + origin[1], %, $seg3)
|
||||
|> xLine(0.63 * scale, %)
|
||||
|> yLine(0.225 * scale, %)
|
||||
|> xLine(-0.57 * scale, %)
|
||||
|> angledLineToX({
|
||||
angle = 47.15,
|
||||
to = 0.93 * scale + origin[0]
|
||||
}, %)
|
||||
|> yLine(0.15 * scale, %)
|
||||
|> xLine(-0.15 * scale, %)
|
||||
|> angledLine({
|
||||
angle = 47.15,
|
||||
length = -segLen(seg1)
|
||||
}, %, $seg2)
|
||||
|> yLine(segLen(seg3), %)
|
||||
|> xLineTo(0 + origin[0], %)
|
||||
|> yLine(-0.225 * scale, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle = 0,
|
||||
intersectTag = seg2,
|
||||
offset = 0
|
||||
}, %)
|
||||
|> close()
|
||||
return zSketch
|
||||
}
|
||||
|
||||
// Define a function to draw the ZOO "O"
|
||||
export fn oLogo(surface, origin, scale) {
|
||||
oSketch001 = surface
|
||||
|> startProfileAt([
|
||||
.788 * scale + origin[0],
|
||||
.921 * scale + origin[1]
|
||||
], %)
|
||||
|> arc({
|
||||
angleStart = 47.15 + 6,
|
||||
angleEnd = 47.15 - 6 + 180,
|
||||
radius = .525 * scale
|
||||
}, %)
|
||||
|> angledLine({ angle = 47.15, length = .24 * scale }, %)
|
||||
|> arc({
|
||||
angleStart = 47.15 - 11 + 180,
|
||||
angleEnd = 47.15 + 11,
|
||||
radius = .288 * scale
|
||||
}, %)
|
||||
|> close()
|
||||
return oSketch001
|
||||
}
|
||||
|
||||
export fn oLogo2(surface, origin, scale) {
|
||||
oSketch002 = surface
|
||||
|> startProfileAt([
|
||||
.16 * scale + origin[0],
|
||||
.079 * scale + origin[1]
|
||||
], %)
|
||||
|> arc({
|
||||
angleStart = 47.15 + 6 - 180,
|
||||
angleEnd = 47.15 - 6,
|
||||
radius = .525 * scale
|
||||
}, %)
|
||||
|> angledLine({ angle = 47.15, length = -.24 * scale }, %)
|
||||
|> arc({
|
||||
angleStart = 47.15 - 11,
|
||||
angleEnd = 47.15 + 11 - 180,
|
||||
radius = .288 * scale
|
||||
}, %)
|
||||
|> close()
|
||||
return oSketch002
|
||||
}
|
8
rust/kcl-python-bindings/justfile
Normal file
8
rust/kcl-python-bindings/justfile
Normal file
@ -0,0 +1,8 @@
|
||||
test:
|
||||
uv pip install .[test]
|
||||
uv run pytest tests/tests.py
|
||||
setup-uv:
|
||||
uv python install
|
||||
uv venv .venv
|
||||
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
|
||||
echo "$PWD/.venv/bin" >> $GITHUB_PATH
|
22
rust/kcl-python-bindings/pyproject.toml
Normal file
22
rust/kcl-python-bindings/pyproject.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[build-system]
|
||||
requires = ["maturin>=1.6,<2.0"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "zoo-kcl"
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Rust",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest",
|
||||
"pytest-asyncio",
|
||||
]
|
||||
|
||||
[tool.maturin]
|
||||
features = ["pyo3/extension-module"]
|
525
rust/kcl-python-bindings/src/lib.rs
Normal file
525
rust/kcl-python-bindings/src/lib.rs
Normal file
@ -0,0 +1,525 @@
|
||||
#![allow(clippy::useless_conversion)]
|
||||
use anyhow::Result;
|
||||
use kcl_lib::{
|
||||
lint::{checks, Discovered},
|
||||
ExecutorContext, UnitLength,
|
||||
};
|
||||
use pyo3::{
|
||||
prelude::PyModuleMethods, pyclass, pyfunction, pymethods, pymodule, types::PyModule, wrap_pyfunction, Bound, PyErr,
|
||||
PyResult,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn tokio() -> &'static tokio::runtime::Runtime {
|
||||
use std::sync::OnceLock;
|
||||
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
|
||||
RT.get_or_init(|| tokio::runtime::Runtime::new().unwrap())
|
||||
}
|
||||
|
||||
fn into_miette(error: kcl_lib::KclErrorWithOutputs) -> PyErr {
|
||||
let report = error.clone().into_miette_report_with_outputs().unwrap();
|
||||
let report = miette::Report::new(report);
|
||||
pyo3::exceptions::PyException::new_err(format!("{:?}", report))
|
||||
}
|
||||
|
||||
fn into_miette_for_parse(filename: &str, input: &str, error: kcl_lib::KclError) -> PyErr {
|
||||
let report = kcl_lib::Report {
|
||||
kcl_source: input.to_string(),
|
||||
error: error.clone(),
|
||||
filename: filename.to_string(),
|
||||
};
|
||||
let report = miette::Report::new(report);
|
||||
pyo3::exceptions::PyException::new_err(format!("{:?}", report))
|
||||
}
|
||||
|
||||
/// The variety of image formats snapshots may be exported to.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Hash, Debug, Clone, Copy)]
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ImageFormat {
|
||||
/// .png format
|
||||
Png,
|
||||
/// .jpeg format
|
||||
Jpeg,
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for kittycad_modeling_cmds::ImageFormat {
|
||||
fn from(format: ImageFormat) -> Self {
|
||||
match format {
|
||||
ImageFormat::Png => kittycad_modeling_cmds::ImageFormat::Png,
|
||||
ImageFormat::Jpeg => kittycad_modeling_cmds::ImageFormat::Jpeg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A file that was exported from the engine.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[pyclass]
|
||||
pub struct ExportFile {
|
||||
/// Binary contents of the file.
|
||||
pub contents: Vec<u8>,
|
||||
/// Name of the file.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<kittycad_modeling_cmds::shared::ExportFile> for ExportFile {
|
||||
fn from(file: kittycad_modeling_cmds::shared::ExportFile) -> Self {
|
||||
ExportFile {
|
||||
contents: file.contents.0,
|
||||
name: file.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kittycad_modeling_cmds::websocket::RawFile> for ExportFile {
|
||||
fn from(file: kittycad_modeling_cmds::websocket::RawFile) -> Self {
|
||||
ExportFile {
|
||||
contents: file.contents,
|
||||
name: file.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ExportFile {
|
||||
#[getter]
|
||||
fn contents(&self) -> Vec<u8> {
|
||||
self.contents.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// The valid types of output file formats.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Hash, Debug, Clone)]
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum FileExportFormat {
|
||||
/// Autodesk Filmbox (FBX) format. <https://en.wikipedia.org/wiki/FBX>
|
||||
Fbx,
|
||||
/// Binary glTF 2.0.
|
||||
///
|
||||
/// This is a single binary with .glb extension.
|
||||
///
|
||||
/// This is better if you want a compressed format as opposed to the human readable glTF that lacks
|
||||
/// compression.
|
||||
Glb,
|
||||
/// glTF 2.0. Embedded glTF 2.0 (pretty printed).
|
||||
///
|
||||
/// Single JSON file with .gltf extension binary data encoded as base64 data URIs.
|
||||
///
|
||||
/// The JSON contents are pretty printed.
|
||||
///
|
||||
/// It is human readable, single file, and you can view the diff easily in a
|
||||
/// git commit.
|
||||
Gltf,
|
||||
/// The OBJ file format. <https://en.wikipedia.org/wiki/Wavefront_.obj_file> It may or
|
||||
/// may not have an an attached material (mtl // mtllib) within the file, but we
|
||||
/// interact with it as if it does not.
|
||||
Obj,
|
||||
/// The PLY file format. <https://en.wikipedia.org/wiki/PLY_(file_format)>
|
||||
Ply,
|
||||
/// The STEP file format. <https://en.wikipedia.org/wiki/ISO_10303-21>
|
||||
Step,
|
||||
/// The STL file format. <https://en.wikipedia.org/wiki/STL_(file_format)>
|
||||
Stl,
|
||||
}
|
||||
|
||||
fn get_output_format(
|
||||
format: &FileExportFormat,
|
||||
src_unit: kittycad_modeling_cmds::units::UnitLength,
|
||||
) -> kittycad_modeling_cmds::format::OutputFormat3d {
|
||||
// Zoo co-ordinate system.
|
||||
//
|
||||
// * Forward: -Y
|
||||
// * Up: +Z
|
||||
// * Handedness: Right
|
||||
let coords = kittycad_modeling_cmds::coord::System {
|
||||
forward: kittycad_modeling_cmds::coord::AxisDirectionPair {
|
||||
axis: kittycad_modeling_cmds::coord::Axis::Y,
|
||||
direction: kittycad_modeling_cmds::coord::Direction::Negative,
|
||||
},
|
||||
up: kittycad_modeling_cmds::coord::AxisDirectionPair {
|
||||
axis: kittycad_modeling_cmds::coord::Axis::Z,
|
||||
direction: kittycad_modeling_cmds::coord::Direction::Positive,
|
||||
},
|
||||
};
|
||||
|
||||
match format {
|
||||
FileExportFormat::Fbx => {
|
||||
kittycad_modeling_cmds::format::OutputFormat3d::Fbx(kittycad_modeling_cmds::format::fbx::export::Options {
|
||||
storage: kittycad_modeling_cmds::format::fbx::export::Storage::Binary,
|
||||
created: None,
|
||||
})
|
||||
}
|
||||
FileExportFormat::Glb => kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
|
||||
kittycad_modeling_cmds::format::gltf::export::Options {
|
||||
storage: kittycad_modeling_cmds::format::gltf::export::Storage::Binary,
|
||||
presentation: kittycad_modeling_cmds::format::gltf::export::Presentation::Compact,
|
||||
},
|
||||
),
|
||||
FileExportFormat::Gltf => kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
|
||||
kittycad_modeling_cmds::format::gltf::export::Options {
|
||||
storage: kittycad_modeling_cmds::format::gltf::export::Storage::Embedded,
|
||||
presentation: kittycad_modeling_cmds::format::gltf::export::Presentation::Pretty,
|
||||
},
|
||||
),
|
||||
FileExportFormat::Obj => {
|
||||
kittycad_modeling_cmds::format::OutputFormat3d::Obj(kittycad_modeling_cmds::format::obj::export::Options {
|
||||
coords,
|
||||
units: src_unit,
|
||||
})
|
||||
}
|
||||
FileExportFormat::Ply => {
|
||||
kittycad_modeling_cmds::format::OutputFormat3d::Ply(kittycad_modeling_cmds::format::ply::export::Options {
|
||||
storage: kittycad_modeling_cmds::format::ply::export::Storage::Ascii,
|
||||
coords,
|
||||
selection: kittycad_modeling_cmds::format::Selection::DefaultScene,
|
||||
units: src_unit,
|
||||
})
|
||||
}
|
||||
FileExportFormat::Step => kittycad_modeling_cmds::format::OutputFormat3d::Step(
|
||||
kittycad_modeling_cmds::format::step::export::Options { coords, created: None },
|
||||
),
|
||||
FileExportFormat::Stl => {
|
||||
kittycad_modeling_cmds::format::OutputFormat3d::Stl(kittycad_modeling_cmds::format::stl::export::Options {
|
||||
storage: kittycad_modeling_cmds::format::stl::export::Storage::Ascii,
|
||||
coords,
|
||||
units: src_unit,
|
||||
selection: kittycad_modeling_cmds::format::Selection::DefaultScene,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the current file from the path given, and read the code.
|
||||
async fn get_code_and_file_path(path: &str) -> Result<(String, std::path::PathBuf)> {
|
||||
let mut path = std::path::PathBuf::from(path);
|
||||
// Check if the path is a directory, if so we want to look for a main.kcl inside.
|
||||
if path.is_dir() {
|
||||
path = path.join("main.kcl");
|
||||
if !path.exists() {
|
||||
return Err(anyhow::anyhow!("Directory must contain a main.kcl file"));
|
||||
}
|
||||
} else {
|
||||
// Otherwise be sure we have a kcl file.
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext != "kcl" {
|
||||
return Err(anyhow::anyhow!("File must have a .kcl extension"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = tokio::fs::read_to_string(&path).await?;
|
||||
Ok((code, path))
|
||||
}
|
||||
|
||||
async fn new_context_state(current_file: Option<std::path::PathBuf>) -> Result<(ExecutorContext, kcl_lib::ExecState)> {
|
||||
let mut settings: kcl_lib::ExecutorSettings = Default::default();
|
||||
if let Some(current_file) = current_file {
|
||||
settings.with_current_file(current_file);
|
||||
}
|
||||
let state = kcl_lib::ExecState::new(&settings);
|
||||
let ctx = ExecutorContext::new_with_client(settings, None, None).await?;
|
||||
Ok((ctx, state))
|
||||
}
|
||||
|
||||
/// Execute the kcl code from a file path.
|
||||
#[pyfunction]
|
||||
async fn execute(path: String) -> PyResult<()> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let (code, path) = get_code_and_file_path(&path)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
let program = kcl_lib::Program::parse_no_errs(&code)
|
||||
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
|
||||
|
||||
let (ctx, mut state) = new_context_state(Some(path))
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
// Execute the program.
|
||||
ctx.run_with_ui_outputs(&program, &mut state)
|
||||
.await
|
||||
.map_err(into_miette)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Execute the kcl code.
|
||||
#[pyfunction]
|
||||
async fn execute_code(code: String) -> PyResult<()> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let program =
|
||||
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
|
||||
|
||||
let (ctx, mut state) = new_context_state(None)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
// Execute the program.
|
||||
ctx.run_with_ui_outputs(&program, &mut state)
|
||||
.await
|
||||
.map_err(into_miette)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Execute a kcl file and snapshot it in a specific format.
|
||||
#[pyfunction]
|
||||
async fn execute_and_snapshot(path: String, image_format: ImageFormat) -> PyResult<Vec<u8>> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let (code, path) = get_code_and_file_path(&path)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
let program = kcl_lib::Program::parse_no_errs(&code)
|
||||
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
|
||||
|
||||
let (ctx, mut state) = new_context_state(Some(path))
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
// Execute the program.
|
||||
ctx.run_with_ui_outputs(&program, &mut state)
|
||||
.await
|
||||
.map_err(into_miette)?;
|
||||
|
||||
// Zoom to fit.
|
||||
ctx.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::ZoomToFit(kittycad_modeling_cmds::ZoomToFit {
|
||||
object_ids: Default::default(),
|
||||
padding: 0.1,
|
||||
animated: false,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Send a snapshot request to the engine.
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::TakeSnapshot(kittycad_modeling_cmds::TakeSnapshot {
|
||||
format: image_format.into(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::TakeSnapshot(data),
|
||||
} = resp
|
||||
else {
|
||||
return Err(pyo3::exceptions::PyException::new_err(format!(
|
||||
"Unexpected response from engine: {:?}",
|
||||
resp
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(data.contents.0)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Execute the kcl code and snapshot it in a specific format.
|
||||
#[pyfunction]
|
||||
async fn execute_code_and_snapshot(code: String, image_format: ImageFormat) -> PyResult<Vec<u8>> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let program =
|
||||
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
|
||||
|
||||
let (ctx, mut state) = new_context_state(None)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
// Execute the program.
|
||||
ctx.run_with_ui_outputs(&program, &mut state)
|
||||
.await
|
||||
.map_err(into_miette)?;
|
||||
|
||||
// Zoom to fit.
|
||||
ctx.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::ZoomToFit(kittycad_modeling_cmds::ZoomToFit {
|
||||
object_ids: Default::default(),
|
||||
padding: 0.1,
|
||||
animated: false,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Send a snapshot request to the engine.
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::TakeSnapshot(kittycad_modeling_cmds::TakeSnapshot {
|
||||
format: image_format.into(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Modeling {
|
||||
modeling_response: kittycad_modeling_cmds::ok_response::OkModelingCmdResponse::TakeSnapshot(data),
|
||||
} = resp
|
||||
else {
|
||||
return Err(pyo3::exceptions::PyException::new_err(format!(
|
||||
"Unexpected response from engine: {:?}",
|
||||
resp
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(data.contents.0)
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Execute a kcl file and export it to a specific file format.
|
||||
#[pyfunction]
|
||||
async fn execute_and_export(path: String, export_format: FileExportFormat) -> PyResult<Vec<ExportFile>> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let (code, path) = get_code_and_file_path(&path)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
let program = kcl_lib::Program::parse_no_errs(&code)
|
||||
.map_err(|err| into_miette_for_parse(&path.display().to_string(), &code, err))?;
|
||||
let settings = program.meta_settings()?.unwrap_or_default();
|
||||
let units: UnitLength = settings.default_length_units.into();
|
||||
|
||||
let (ctx, mut state) = new_context_state(Some(path))
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
// Execute the program.
|
||||
ctx.run_with_ui_outputs(&program, &mut state)
|
||||
.await
|
||||
.map_err(into_miette)?;
|
||||
|
||||
// This will not return until there are files.
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
|
||||
entity_ids: vec![],
|
||||
format: get_output_format(&export_format, units.into()),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
|
||||
return Err(pyo3::exceptions::PyException::new_err(format!(
|
||||
"Unexpected response from engine: {:?}",
|
||||
resp
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(files.into_iter().map(ExportFile::from).collect())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Execute the kcl code and export it to a specific file format.
|
||||
#[pyfunction]
|
||||
async fn execute_code_and_export(code: String, export_format: FileExportFormat) -> PyResult<Vec<ExportFile>> {
|
||||
tokio()
|
||||
.spawn(async move {
|
||||
let program =
|
||||
kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
|
||||
let settings = program.meta_settings()?.unwrap_or_default();
|
||||
let units: UnitLength = settings.default_length_units.into();
|
||||
|
||||
let (ctx, mut state) = new_context_state(None)
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
// Execute the program.
|
||||
ctx.run_with_ui_outputs(&program, &mut state)
|
||||
.await
|
||||
.map_err(into_miette)?;
|
||||
|
||||
// This will not return until there are files.
|
||||
let resp = ctx
|
||||
.engine
|
||||
.send_modeling_cmd(
|
||||
uuid::Uuid::new_v4(),
|
||||
kcl_lib::SourceRange::default(),
|
||||
&kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
|
||||
entity_ids: vec![],
|
||||
format: get_output_format(&export_format, units.into()),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
|
||||
return Err(pyo3::exceptions::PyException::new_err(format!(
|
||||
"Unexpected response from engine: {:?}",
|
||||
resp
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(files.into_iter().map(ExportFile::from).collect())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?
|
||||
}
|
||||
|
||||
/// Format the kcl code.
|
||||
#[pyfunction]
|
||||
fn format(code: String) -> PyResult<String> {
|
||||
let program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
|
||||
let recasted = program.recast();
|
||||
|
||||
Ok(recasted)
|
||||
}
|
||||
|
||||
/// Lint the kcl code.
|
||||
#[pyfunction]
|
||||
fn lint(code: String) -> PyResult<Vec<Discovered>> {
|
||||
let program = kcl_lib::Program::parse_no_errs(&code).map_err(|err| into_miette_for_parse("", &code, err))?;
|
||||
let lints = program
|
||||
.lint(checks::lint_variables)
|
||||
.map_err(|err| pyo3::exceptions::PyException::new_err(err.to_string()))?;
|
||||
|
||||
Ok(lints)
|
||||
}
|
||||
|
||||
/// The kcl python module.
|
||||
#[pymodule]
|
||||
fn kcl(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
// Add our types to the module.
|
||||
m.add_class::<ImageFormat>()?;
|
||||
m.add_class::<ExportFile>()?;
|
||||
m.add_class::<FileExportFormat>()?;
|
||||
m.add_class::<UnitLength>()?;
|
||||
m.add_class::<Discovered>()?;
|
||||
|
||||
// Add our functions to the module.
|
||||
m.add_function(wrap_pyfunction!(execute, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_code, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_and_snapshot, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_code_and_snapshot, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_and_export, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(execute_code_and_export, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(format, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(lint, m)?)?;
|
||||
Ok(())
|
||||
}
|
140
rust/kcl-python-bindings/tests/tests.py
Executable file
140
rust/kcl-python-bindings/tests/tests.py
Executable file
@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
|
||||
import kcl
|
||||
import pytest
|
||||
|
||||
# Get the path to this script's parent directory.
|
||||
files_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "files")
|
||||
kcl_dir = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), "..", "..", "kcl-lib"
|
||||
)
|
||||
lego_file = os.path.join(kcl_dir, "e2e", "executor", "inputs", "lego.kcl")
|
||||
walkie_talkie_dir = os.path.join(files_dir, "walkie-talkie")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_with_exception():
|
||||
# Read from a file.
|
||||
try:
|
||||
await kcl.execute(os.path.join(files_dir, "parse_file_error"))
|
||||
except Exception as e:
|
||||
assert e is not None
|
||||
assert len(str(e)) > 0
|
||||
assert "lksjndflsskjfnak;jfna##" in str(e)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute():
|
||||
# Read from a file.
|
||||
await kcl.execute(lego_file)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_code():
|
||||
# Read from a file.
|
||||
with open(lego_file, "r") as f:
|
||||
code = str(f.read())
|
||||
assert code is not None
|
||||
assert len(code) > 0
|
||||
await kcl.execute_code(code)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_code_and_snapshot():
|
||||
# Read from a file.
|
||||
with open(lego_file, "r") as f:
|
||||
code = str(f.read())
|
||||
assert code is not None
|
||||
assert len(code) > 0
|
||||
image_bytes = await kcl.execute_code_and_snapshot(code, kcl.ImageFormat.Jpeg)
|
||||
assert image_bytes is not None
|
||||
assert len(image_bytes) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_code_and_export():
|
||||
# Read from a file.
|
||||
with open(lego_file, "r") as f:
|
||||
code = str(f.read())
|
||||
assert code is not None
|
||||
assert len(code) > 0
|
||||
files = await kcl.execute_code_and_export(code, kcl.FileExportFormat.Step)
|
||||
assert files is not None
|
||||
assert len(files) > 0
|
||||
assert files[0] is not None
|
||||
name = files[0].name
|
||||
contents = files[0].contents
|
||||
assert name is not None
|
||||
assert len(name) > 0
|
||||
assert contents is not None
|
||||
assert len(contents) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_dir_assembly():
|
||||
# Read from a file.
|
||||
await kcl.execute(walkie_talkie_dir)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_and_snapshot():
|
||||
# Read from a file.
|
||||
image_bytes = await kcl.execute_and_snapshot(lego_file, kcl.ImageFormat.Jpeg)
|
||||
assert image_bytes is not None
|
||||
assert len(image_bytes) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_and_snapshot_dir():
|
||||
# Read from a file.
|
||||
image_bytes = await kcl.execute_and_snapshot(
|
||||
walkie_talkie_dir, kcl.ImageFormat.Jpeg
|
||||
)
|
||||
assert image_bytes is not None
|
||||
assert len(image_bytes) > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kcl_execute_and_export():
|
||||
# Read from a file.
|
||||
files = await kcl.execute_and_export(lego_file, kcl.FileExportFormat.Step)
|
||||
assert files is not None
|
||||
assert len(files) > 0
|
||||
assert files[0] is not None
|
||||
name = files[0].name
|
||||
contents = files[0].contents
|
||||
assert name is not None
|
||||
assert len(name) > 0
|
||||
assert contents is not None
|
||||
assert len(contents) > 0
|
||||
|
||||
|
||||
def test_kcl_format():
|
||||
# Read from a file.
|
||||
with open(lego_file, "r") as f:
|
||||
code = str(f.read())
|
||||
assert code is not None
|
||||
assert len(code) > 0
|
||||
formatted_code = kcl.format(code)
|
||||
assert formatted_code is not None
|
||||
assert len(formatted_code) > 0
|
||||
|
||||
|
||||
def test_kcl_lint():
|
||||
# Read from a file.
|
||||
with open(os.path.join(files_dir, "box_with_linter_errors.kcl"), "r") as f:
|
||||
code = str(f.read())
|
||||
assert code is not None
|
||||
assert len(code) > 0
|
||||
lints = kcl.lint(code)
|
||||
assert lints is not None
|
||||
assert len(lints) > 0
|
||||
description = lints[0].description
|
||||
assert description is not None
|
||||
assert len(description) > 0
|
||||
finding = lints[0].finding
|
||||
assert finding is not None
|
||||
finding_title = finding.title
|
||||
assert finding_title is not None
|
||||
assert len(finding_title) > 0
|
@ -13,8 +13,8 @@ name = "kcl-to-core"
|
||||
path = "src/tool.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1.85"
|
||||
anyhow = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
kcl-lib = { path = "../kcl-lib" }
|
||||
kittycad = { workspace = true, features = ["clap"] }
|
||||
|
Reference in New Issue
Block a user