Compare commits

...

13 Commits

Author SHA1 Message Date
aa0b97de3d Merge branch 'main' into marijnh/signature-tooltip 2025-05-12 15:56:08 -07:00
95a02cbcd7 Check all nested KCL samples (#6880) 2025-05-12 18:43:51 -04:00
a049768f1c Move some more functions to be declared in KCL (#6856)
* Move the leg functions to KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Move array functions to KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Move clone to KCL

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Add a function type

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-05-13 08:29:38 +12:00
818d9a0d77 Release link and updater toast touchups on Nightly (#6881)
pierremtb/adhoc/clean-up-nightly-on-merge-more
2025-05-12 20:20:12 +00:00
1a8f80a7dc #6793 Fix file, folder renaming issues (#6869)
* fix going into rename mode for files with parents

* lastDirectoryClicked is not used

* fix file/folder rename bugs: renaming within folders

* Turn form into div to fix issue of child renaming continues into renaming the parent folder when hitting enter

* ContextMenu stopPropagation not needed anymore, maybe because of form refactor

* ContextMenu IS needed actually, with multiple nested folders

* make lint happy

* Update src/components/ContextMenu.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* re-add <form> instead of <div> for file renaming

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-05-12 21:59:10 +02:00
566c9eaf10 Spell out shortcut in project search bar (#6872)
dumb little zero-thought bit of polish.
2025-05-12 19:36:05 +00:00
e6485c2da1 Clean up Nightly release version & notes (#6877)
* pierremtb/adhoc/clean-up-nightly-on-merge

* To revert: test to upload builds at test/nightly

* Remove + suffix trim that's not needed anymore

* Revert "To revert: test to upload builds at test/nightly"

This reverts commit b0549d426f.
2025-05-12 14:21:09 -04:00
0479edd36a Remove unused GitHub Actions workflow for old onboarding (#6873) 2025-05-12 17:30:51 +00:00
87c1e92134 Deploy Nightly on merge to main (#6871)
* Deploy Nightly on merge to main

* Forgot one block

* Remove .github/workflows/tag-nightly.yml

* Remove scripts/get-nightly-changelog.sh

* Update .github/workflows/build-apps.yml

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-12 12:30:01 -04:00
8f950ac1b0 Always run end-to-end tests (#6870) 2025-05-12 12:23:09 -04:00
78e4f43708 Fix kcl errors indicator not scaling with multi-digit count (#6864)
Fixes #6847
2025-05-12 11:36:09 -04:00
24a241c05a Use a CodeMirror tooltip for the signature help in lsp-client 2025-05-10 08:30:34 +02:00
9265af9115 Small simplifications in LSP extensions 2025-05-10 08:11:25 +02:00
87 changed files with 1164 additions and 36178 deletions

View File

@ -7,11 +7,10 @@ on:
- main
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'nightly-v[0-9]+.[0-9]+.[0-9]+'
env:
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
IS_NIGHTLY: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'nightly-v') }}
IS_NIGHTLY: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -95,7 +94,9 @@ jobs:
- name: Set nightly version, product name, release notes, and icons
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
export VERSION=${GITHUB_REF_NAME#nightly-v}
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +'%-y.%-m.%-d')
export VERSION=$DATE-main.$COMMIT
npm run files:set-version
npm run files:flip-to-nightly
@ -306,7 +307,8 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
if: ${{ github.ref_type == 'tag' }}
# Equivalent to IS_RELEASE || IS_NIGHTLY (but we can't access those env vars here)
if: ${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ format('v{0}', needs.prepare-files.outputs.version) }}
@ -412,17 +414,6 @@ jobs:
- name: List artifacts
run: "ls -R out"
- name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
# Note: preferred going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}"
export PREVIOUS_TAG=$(git tag --list --sort=-committerdate "nightly-v[0-9]*" | head -n2 | tail -n1)
export NOTES=$(./scripts/get-nightly-changelog.sh)
npm run files:set-notes
- name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.8'

View File

@ -1,47 +0,0 @@
name: Check Onboarding KCL
on:
pull_request:
types: [opened, synchronize]
paths:
- 'src/lib/exampleKcl.ts'
- 'public/kcl-samples/bracket/main.kcl'
permissions:
contents: read
issues: write
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const message = '`public/kcl-samples/bracket/main.kcl` or `src/lib/exampleKcl.ts` has been updated in this PR, please review and update the `src/routes/onboarding`, if needed.';
const issue_number = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number
});
const commentExists = comments.some(comment => comment.body === message);
if (!commentExists) {
// Post a comment on the PR
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body: message,
});
}

View File

@ -18,73 +18,13 @@ permissions:
jobs:
conditions:
runs-on: ubuntu-latest
outputs:
significant: ${{ steps.path-changes.outputs.significant }}
should-run: ${{ steps.should-run.outputs.should-run }}
steps:
- uses: actions/checkout@v4
- name: Fetch the base branch
if: ${{ github.event_name == 'pull_request' }}
run: git fetch origin ${{ github.base_ref }} --depth=1
- name: Check for path changes
id: path-changes
shell: bash
run: |
set -euo pipefail
# Manual runs or push should run all tests.
if [[ ${{ github.event_name }} != 'pull_request' ]]; then
echo "significant=true" >> $GITHUB_OUTPUT
exit 0
fi
changed_files=$(git diff --name-only origin/${{ github.base_ref }})
echo "$changed_files"
if grep -Evq '^README.md|^public/kcl-samples/|^rust/kcl-lib/tests/kcl_samples/' <<< "$changed_files" ; then
echo "significant=true" >> $GITHUB_OUTPUT
else
echo "significant=false" >> $GITHUB_OUTPUT
fi
- name: Should run
id: should-run
shell: bash
run: |
set -euo pipefail
# We should run when this is a scheduled run or if there are
# significant changes in the diff.
if [[ ${{ github.event_name }} == 'schedule' || ${{ steps.path-changes.outputs.significant }} == 'true' ]]; then
echo "should-run=true" >> $GITHUB_OUTPUT
else
echo "should-run=false" >> $GITHUB_OUTPUT
fi
- name: Display conditions
shell: bash
run: |
# For debugging purposes
set -euo pipefail
echo "GITHUB_REF: $GITHUB_REF"
echo "GITHUB_HEAD_REF: $GITHUB_HEAD_REF"
echo "GITHUB_BASE_REF: $GITHUB_BASE_REF"
echo "significant: ${{ steps.path-changes.outputs.significant }}"
echo "should-run: ${{ steps.should-run.outputs.should-run }}"
prepare-wasm:
# separate job on Ubuntu to build or fetch the wasm blob once on the fastest runner
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
needs: conditions
steps:
- uses: actions/checkout@v4
if: needs.conditions.outputs.should-run == 'true'
- id: filter
if: needs.conditions.outputs.should-run == 'true'
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
@ -93,18 +33,16 @@ jobs:
- 'rust/**'
- uses: actions/setup-node@v4
if: needs.conditions.outputs.should-run == 'true'
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
if: needs.conditions.outputs.should-run == 'true'
run: npm install
- name: Download Wasm Cache
id: download-wasm
if: ${{ needs.conditions.outputs.should-run == 'true' && github.event_name != 'schedule' && steps.filter.outputs.rust == 'false' }}
if: ${{ github.event_name != 'schedule' && steps.filter.outputs.rust == 'false' }}
uses: dawidd6/action-download-artifact@v7
continue-on-error: true
with:
@ -116,7 +54,6 @@ jobs:
- name: Build WASM condition
id: wasm
if: needs.conditions.outputs.should-run == 'true'
run: |
set -euox pipefail
# Build wasm if this is a scheduled run, there are Rust changes, or
@ -128,35 +65,34 @@ jobs:
fi
- name: Use correct Rust toolchain
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
with:
tool: wasm-pack
- name: Rust Cache
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
uses: Swatinem/rust-cache@v2
with:
workspaces: './rust'
- name: Build Wasm
if: ${{ needs.conditions.outputs.should-run == 'true' && steps.wasm.outputs.should-build-wasm == 'true' }}
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
shell: bash
run: npm run build:wasm
- uses: actions/upload-artifact@v4
if: needs.conditions.outputs.should-run == 'true'
with:
name: prepared-wasm
path: |
@ -165,10 +101,9 @@ jobs:
snapshots:
name: playwright:snapshots:ubuntu
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
needs: [conditions, prepare-wasm]
needs: [prepare-wasm]
steps:
- uses: actions/create-github-app-token@v1
if: needs.conditions.outputs.should-run == 'true'
id: app-token
with:
app-id: ${{ secrets.MODELING_APP_GH_APP_ID }}
@ -176,16 +111,13 @@ jobs:
owner: ${{ github.repository_owner }}
- uses: actions/checkout@v4
if: needs.conditions.outputs.should-run == 'true'
with:
token: ${{ steps.app-token.outputs.token }}
- uses: actions/download-artifact@v4
if: needs.conditions.outputs.should-run == 'true'
name: prepared-wasm
- name: Copy prepared wasm
if: needs.conditions.outputs.should-run == 'true'
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
@ -193,18 +125,15 @@ jobs:
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- uses: actions/setup-node@v4
if: needs.conditions.outputs.should-run == 'true'
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
id: deps-install
if: needs.conditions.outputs.should-run == 'true'
run: npm install
- name: Cache Playwright Browsers
if: needs.conditions.outputs.should-run == 'true'
uses: actions/cache@v4
with:
path: |
@ -212,15 +141,12 @@ jobs:
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
- name: Install Playwright Browsers
if: needs.conditions.outputs.should-run == 'true'
run: npm run playwright install --with-deps
- name: build web
if: needs.conditions.outputs.should-run == 'true'
run: npm run tronb:vite:dev
- name: Run ubuntu/chrome snapshots
if: needs.conditions.outputs.should-run == 'true'
uses: nick-fields/retry@v3.0.2
with:
shell: bash
@ -236,7 +162,7 @@ jobs:
TARGET: web
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
if: ${{ !cancelled() && (success() || failure()) }}
with:
name: playwright-report-ubuntu-snapshot-${{ github.sha }}
path: playwright-report/
@ -245,7 +171,7 @@ jobs:
overwrite: true
- name: Check for changes
if: ${{ needs.conditions.outputs.should-run == 'true' && github.ref != 'refs/heads/main' }}
if: ${{ github.ref != 'refs/heads/main' }}
shell: bash
id: git-check
run: |
@ -257,7 +183,7 @@ jobs:
- name: Commit changes, if any
# TODO: find a more reliable way to detect visual changes
if: ${{ false && needs.conditions.outputs.should-run == 'true' && steps.git-check.outputs.modified == 'true' }}
if: ${{ false && steps.git-check.outputs.modified == 'true' }}
shell: bash
run: |
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
@ -272,7 +198,7 @@ jobs:
git push origin ${{ github.head_ref }}
electron:
needs: [conditions, prepare-wasm]
needs: [prepare-wasm]
timeout-minutes: 60
env:
OS_NAME: ${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}
@ -315,14 +241,11 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
if: needs.conditions.outputs.should-run == 'true'
- uses: actions/download-artifact@v4
if: needs.conditions.outputs.should-run == 'true'
name: prepared-wasm
- name: Copy prepared wasm
if: needs.conditions.outputs.should-run == 'true'
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
@ -330,18 +253,15 @@ jobs:
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- uses: actions/setup-node@v4
if: needs.conditions.outputs.should-run == 'true'
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
id: deps-install
if: needs.conditions.outputs.should-run == 'true'
run: npm install
- name: Cache Playwright Browsers
if: needs.conditions.outputs.should-run == 'true'
uses: actions/cache@v4
with:
path: |
@ -349,22 +269,20 @@ jobs:
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
- name: Install Playwright Browsers
if: needs.conditions.outputs.should-run == 'true'
run: npm run playwright install --with-deps
- name: Build web
if: needs.conditions.outputs.should-run == 'true'
run: npm run tronb:vite:dev
- name: Start Vector
if: ${{ needs.conditions.outputs.should-run == 'true' && !contains(matrix.os, 'windows') }}
if: ${{ !contains(matrix.os, 'windows') }}
run: .github/ci-cd-scripts/start-vector-${{ env.OS_NAME }}.sh
env:
GH_ACTIONS_AXIOM_TOKEN: ${{ secrets.GH_ACTIONS_AXIOM_TOKEN }}
OS_NAME: ${{ env.OS_NAME }}
- uses: actions/download-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && (success() || failure()) }}
if: ${{ !cancelled() && (success() || failure()) }}
continue-on-error: true
with:
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
@ -372,7 +290,7 @@ jobs:
- name: Run playwright/electron flow (with retries)
id: retry
if: ${{ needs.conditions.outputs.should-run == 'true' && !cancelled() && steps.deps-install.outcome == 'success' }}
if: ${{ !cancelled() && steps.deps-install.outcome == 'success' }}
uses: nick-fields/retry@v3.0.2
with:
shell: bash
@ -389,7 +307,7 @@ jobs:
TARGET: desktop
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && always() }}
if: always()
with:
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
@ -398,7 +316,7 @@ jobs:
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ needs.conditions.outputs.should-run == 'true' && always() }}
if: always()
with:
name: playwright-report-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/

View File

@ -1,39 +0,0 @@
name: tag-nightly
permissions:
contents: write
on:
schedule:
- cron: '0 4 * * *'
# Daily at 04:00 AM UTC
# Will checkout the last commit from the default branch (main as of 2023-10-04)
jobs:
tag-nightly:
runs-on: ubuntu-22.04
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.MODELING_APP_GH_APP_ID }}
private-key: ${{ secrets.MODELING_APP_GH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: npm install
- name: Push tag
run: |
VERSION_NO_V=$(date +'%-y.%-m.%-d')
TAG="nightly-v$VERSION_NO_V"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git tag $TAG
git push origin tag $TAG

View File

@ -23,6 +23,7 @@ endif
install: node_modules/.package-lock.json $(CARGO) $(WASM_PACK) ## Install dependencies
node_modules/.package-lock.json: package.json package-lock.json
npm prune
npm install
$(CARGO):
@ -43,15 +44,15 @@ endif
# BUILD
CARGO_SOURCES := rust/.cargo/config.toml $(wildcard rust/Cargo.*) $(wildcard rust/**/Cargo.*)
KCL_SOURCES := $(wildcard public/kcl-samples/**/*.kcl)
RUST_SOURCES := $(wildcard rust/**/*.rs)
REACT_SOURCES := $(wildcard src/*.tsx) $(wildcard src/**/*.tsx)
TYPESCRIPT_SOURCES := tsconfig.* $(wildcard src/*.ts) $(wildcard src/**/*.ts)
VITE_SOURCES := $(wildcard vite.*) $(wildcard vite/**/*.tsx)
.PHONY: build
build: install public/kcl_wasm_lib_bg.wasm .vite/build/main.js
build: install public/kcl_wasm_lib_bg.wasm public/kcl-samples/manifest.json .vite/build/main.js
public/kcl_wasm_lib_bg.wasm: $(CARGO_SOURCES) $(RUST_SOURCES)
ifdef WINDOWS
@ -60,6 +61,9 @@ else
npm run build:wasm:dev
endif
public/kcl-samples/manifest.json: $(KCL_SOURCES)
cd rust/kcl-lib && EXPECTORATE=overwrite cargo test generate_manifest
.vite/build/main.js: $(REACT_SOURCES) $(TYPESCRIPT_SOURCES) $(VITE_SOURCES)
npm run tronb:vite:dev
@ -107,8 +111,10 @@ test: test-unit test-e2e
.PHONY: test-unit
test-unit: install ## Run the unit tests
npm run test:rust
npm run test:unit:components
@ curl -fs localhost:3000 >/dev/null || ( echo "Error: localhost:3000 not available, 'make run-web' first" && exit 1 )
npm run test:unit
npm run test:unit:kcl-samples
.PHONY: test-e2e
test-e2e: test-e2e-$(TARGET)

View File

@ -22,14 +22,14 @@ This will work on any solid, including extruded solids, revolved solids, and she
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `solids` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) | The solid(s) whose appearance is being set | Yes |
| `solids` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) | The solid(s) whose appearance is being set | Yes |
| `color` | [`string`](/docs/kcl-std/types/std-types-string) | Color of the new material, a hex string like '#ff0000' | Yes |
| `metalness` | [`number`](/docs/kcl-std/types/std-types-number) | Metalness of the new material, a percentage like 95.7. | No |
| `roughness` | [`number`](/docs/kcl-std/types/std-types-number) | Roughness of the new material, a percentage like 95.7. | No |
### Returns
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) - Data for a solid or an imported geometry.
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) - Data for a solid or an imported geometry.
### Examples

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,38 @@
---
title: "legAngX"
subtitle: "Function in std::math"
excerpt: "Compute the angle of the given leg for x."
layout: manual
---
Compute the angle of the given leg for x.
```kcl
legAngX(
hypotenuse: number(Length),
leg: number(Length),
): number(deg)
```
### Arguments
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The length of the triangle's hypotenuse. | Yes |
| `leg` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The length of one of the triangle's legs (i.e. non-hypotenuse side). | Yes |
### Returns
[`number(deg)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
```kcl
legAngX(hypotenuse = 5, leg = 3)
```

View File

@ -0,0 +1,38 @@
---
title: "legAngY"
subtitle: "Function in std::math"
excerpt: "Compute the angle of the given leg for y."
layout: manual
---
Compute the angle of the given leg for y.
```kcl
legAngY(
hypotenuse: number(Length),
leg: number(Length),
): number(deg)
```
### Arguments
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The length of the triangle's hypotenuse. | Yes |
| `leg` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The length of one of the triangle's legs (i.e. non-hypotenuse side). | Yes |
### Returns
[`number(deg)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
```kcl
legAngY(hypotenuse = 5, leg = 3)
```

View File

@ -0,0 +1,38 @@
---
title: "legLen"
subtitle: "Function in std::math"
excerpt: "Compute the length of the given leg."
layout: manual
---
Compute the length of the given leg.
```kcl
legLen(
hypotenuse: number(Length),
leg: number(Length),
): number(deg)
```
### Arguments
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The length of the triangle's hypotenuse. | Yes |
| `leg` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The length of one of the triangle's legs (i.e. non-hypotenuse side). | Yes |
### Returns
[`number(deg)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
```kcl
legLen(hypotenuse = 5, leg = 3)
```

View File

@ -12,15 +12,15 @@ layout: manual
* [`appearance`](/docs/kcl-std/appearance)
* [`assert`](/docs/kcl-std/assert)
* [`assertIs`](/docs/kcl-std/assertIs)
* [`clone`](/docs/kcl-std/clone)
* [`clone`](/docs/kcl-std/functions/std-clone)
* [`helix`](/docs/kcl-std/functions/std-helix)
* [`offsetPlane`](/docs/kcl-std/functions/std-offsetPlane)
* [`patternLinear2d`](/docs/kcl-std/patternLinear2d)
* [**std::array**](/docs/kcl-std/modules/std-array)
* [`map`](/docs/kcl-std/map)
* [`pop`](/docs/kcl-std/pop)
* [`push`](/docs/kcl-std/push)
* [`reduce`](/docs/kcl-std/reduce)
* [`map`](/docs/kcl-std/functions/std-array-map)
* [`pop`](/docs/kcl-std/functions/std-array-pop)
* [`push`](/docs/kcl-std/functions/std-array-push)
* [`reduce`](/docs/kcl-std/functions/std-array-reduce)
* [**std::math**](/docs/kcl-std/modules/std-math)
* [`abs`](/docs/kcl-std/functions/std-math-abs)
* [`acos`](/docs/kcl-std/functions/std-math-acos)
@ -30,9 +30,9 @@ layout: manual
* [`ceil`](/docs/kcl-std/functions/std-math-ceil)
* [`cos`](/docs/kcl-std/functions/std-math-cos)
* [`floor`](/docs/kcl-std/functions/std-math-floor)
* [`legAngX`](/docs/kcl-std/legAngX)
* [`legAngY`](/docs/kcl-std/legAngY)
* [`legLen`](/docs/kcl-std/legLen)
* [`legAngX`](/docs/kcl-std/functions/std-math-legAngX)
* [`legAngY`](/docs/kcl-std/functions/std-math-legAngY)
* [`legLen`](/docs/kcl-std/functions/std-math-legLen)
* [`ln`](/docs/kcl-std/functions/std-math-ln)
* [`log`](/docs/kcl-std/functions/std-math-log)
* [`log10`](/docs/kcl-std/functions/std-math-log10)
@ -140,7 +140,8 @@ See also the [types overview](/docs/kcl-lang/types)
* [**Primitive types**](/docs/kcl-lang/types)
* [`End`](/docs/kcl-lang/types#End)
* [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry)
* [`Fn`](/docs/kcl-std/types/std-types-Fn)
* [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry)
* [`Start`](/docs/kcl-lang/types#Start)
* [`TagDeclarator`](/docs/kcl-lang/types#TagDeclarator)
* [`TagIdentifier`](/docs/kcl-lang/types#TagIdentifier)

View File

@ -1,38 +0,0 @@
---
title: "legAngX"
subtitle: "Function in std::math"
excerpt: "Compute the angle of the given leg for x."
layout: manual
---
Compute the angle of the given leg for x.
```kcl
legAngX(
hypotenuse: number,
leg: number,
): number
```
### Arguments
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number`](/docs/kcl-std/types/std-types-number) | The length of the triangle's hypotenuse | Yes |
| `leg` | [`number`](/docs/kcl-std/types/std-types-number) | The length of one of the triangle's legs (i.e. non-hypotenuse side) | Yes |
### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
```kcl
legAngX(hypotenuse = 5, leg = 3)
```

View File

@ -1,38 +0,0 @@
---
title: "legAngY"
subtitle: "Function in std::math"
excerpt: "Compute the angle of the given leg for y."
layout: manual
---
Compute the angle of the given leg for y.
```kcl
legAngY(
hypotenuse: number,
leg: number,
): number
```
### Arguments
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number`](/docs/kcl-std/types/std-types-number) | The length of the triangle's hypotenuse | Yes |
| `leg` | [`number`](/docs/kcl-std/types/std-types-number) | The length of one of the triangle's legs (i.e. non-hypotenuse side) | Yes |
### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
```kcl
legAngY(hypotenuse = 5, leg = 3)
```

View File

@ -1,38 +0,0 @@
---
title: "legLen"
subtitle: "Function in std::math"
excerpt: "Compute the length of the given leg."
layout: manual
---
Compute the length of the given leg.
```kcl
legLen(
hypotenuse: number,
leg: number,
): number
```
### Arguments
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `hypotenuse` | [`number`](/docs/kcl-std/types/std-types-number) | The length of the triangle's hypotenuse | Yes |
| `leg` | [`number`](/docs/kcl-std/types/std-types-number) | The length of one of the triangle's legs (i.e. non-hypotenuse side) | Yes |
### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number.
### Examples
```kcl
legLen(hypotenuse = 5, leg = 3)
```

View File

@ -12,8 +12,8 @@ Functions for manipulating arrays of values.
## Functions and constants
* [`map`](/docs/kcl-std/map)
* [`pop`](/docs/kcl-std/pop)
* [`push`](/docs/kcl-std/push)
* [`reduce`](/docs/kcl-std/reduce)
* [`map`](/docs/kcl-std/functions/std-array-map)
* [`pop`](/docs/kcl-std/functions/std-array-pop)
* [`push`](/docs/kcl-std/functions/std-array-push)
* [`reduce`](/docs/kcl-std/functions/std-array-reduce)

View File

@ -23,9 +23,9 @@ Functions for mathematical operations and some useful constants.
* [`ceil`](/docs/kcl-std/functions/std-math-ceil)
* [`cos`](/docs/kcl-std/functions/std-math-cos)
* [`floor`](/docs/kcl-std/functions/std-math-floor)
* [`legAngX`](/docs/kcl-std/legAngX)
* [`legAngY`](/docs/kcl-std/legAngY)
* [`legLen`](/docs/kcl-std/legLen)
* [`legAngX`](/docs/kcl-std/functions/std-math-legAngX)
* [`legAngY`](/docs/kcl-std/functions/std-math-legAngY)
* [`legLen`](/docs/kcl-std/functions/std-math-legLen)
* [`ln`](/docs/kcl-std/functions/std-math-ln)
* [`log`](/docs/kcl-std/functions/std-math-log)
* [`log10`](/docs/kcl-std/functions/std-math-log10)

View File

@ -17,7 +17,9 @@ Types can (optionally) be used to describe a function's arguments and returned v
* [`Axis3d`](/docs/kcl-std/types/std-types-Axis3d)
* [`Edge`](/docs/kcl-std/types/std-types-Edge)
* [`Face`](/docs/kcl-std/types/std-types-Face)
* [`Fn`](/docs/kcl-std/types/std-types-Fn)
* [`Helix`](/docs/kcl-std/types/std-types-Helix)
* [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry)
* [`Plane`](/docs/kcl-std/types/std-types-Plane)
* [`Point2d`](/docs/kcl-std/types/std-types-Point2d)
* [`Point3d`](/docs/kcl-std/types/std-types-Point3d)

View File

@ -37,7 +37,7 @@ You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL gui
* [`appearance`](/docs/kcl-std/appearance)
* [`assert`](/docs/kcl-std/assert)
* [`assertIs`](/docs/kcl-std/assertIs)
* [`clone`](/docs/kcl-std/clone)
* [`clone`](/docs/kcl-std/functions/std-clone)
* [`helix`](/docs/kcl-std/functions/std-helix)
* [`offsetPlane`](/docs/kcl-std/functions/std-offsetPlane)
* [`patternLinear2d`](/docs/kcl-std/patternLinear2d)

View File

@ -43,7 +43,7 @@ When rotating a part around an axis, you specify the axis of rotation and the an
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `objects` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) | The solid, sketch, or set of solids or sketches to rotate. | Yes |
| `objects` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) | The solid, sketch, or set of solids or sketches to rotate. | Yes |
| `roll` | [`number`](/docs/kcl-std/types/std-types-number) | The roll angle in degrees. Must be between -360 and 360. Default is 0 if not given. | No |
| `pitch` | [`number`](/docs/kcl-std/types/std-types-number) | The pitch angle in degrees. Must be between -360 and 360. Default is 0 if not given. | No |
| `yaw` | [`number`](/docs/kcl-std/types/std-types-number) | The yaw angle in degrees. Must be between -360 and 360. Default is 0 if not given. | No |
@ -53,7 +53,7 @@ When rotating a part around an axis, you specify the axis of rotation and the an
### Returns
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) - Data for a solid, sketch, or an imported geometry.
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -29,7 +29,7 @@ If you want to apply the transform in global space, set `global` to `true`. The
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `objects` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) | The solid, sketch, or set of solids or sketches to scale. | Yes |
| `objects` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) | The solid, sketch, or set of solids or sketches to scale. | Yes |
| `x` | [`number`](/docs/kcl-std/types/std-types-number) | The scale factor for the x axis. Default is 1 if not provided. | No |
| `y` | [`number`](/docs/kcl-std/types/std-types-number) | The scale factor for the y axis. Default is 1 if not provided. | No |
| `z` | [`number`](/docs/kcl-std/types/std-types-number) | The scale factor for the z axis. Default is 1 if not provided. | No |
@ -37,7 +37,7 @@ If you want to apply the transform in global space, set `global` to `true`. The
### Returns
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) - Data for a solid, sketch, or an imported geometry.
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,7 @@ Translate is really useful for sketches if you want to move a sketch and then ro
| Name | Type | Description | Required |
|----------|------|-------------|----------|
| `objects` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) | The solid, sketch, or set of solids or sketches to move. | Yes |
| `objects` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) | The solid, sketch, or set of solids or sketches to move. | Yes |
| `x` | [`number`](/docs/kcl-std/types/std-types-number) | The amount to move the solid or sketch along the x axis. Defaults to 0 if not provided. | No |
| `y` | [`number`](/docs/kcl-std/types/std-types-number) | The amount to move the solid or sketch along the y axis. Defaults to 0 if not provided. | No |
| `z` | [`number`](/docs/kcl-std/types/std-types-number) | The amount to move the solid or sketch along the z axis. Defaults to 0 if not provided. | No |
@ -33,7 +33,7 @@ Translate is really useful for sketches if you want to move a sketch and then ro
### Returns
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-lang/types#ImportedGeometry) - Data for a solid, sketch, or an imported geometry.
[`[Solid]`](/docs/kcl-std/types/std-types-Solid) or [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) or [`ImportedGeometry`](/docs/kcl-std/types/std-types-ImportedGeometry) - Data for a solid, sketch, or an imported geometry.
### Examples

View File

@ -0,0 +1,13 @@
---
title: "Fn"
subtitle: "Type in std::types"
excerpt: "The type of any function in KCL."
layout: manual
---
The type of any function in KCL.

View File

@ -0,0 +1,13 @@
---
title: "ImportedGeometry"
subtitle: "Type in std::types"
excerpt: "Represents geometry which is defined using some other CAD system and imported into KCL."
layout: manual
---
Represents geometry which is defined using some other CAD system and imported into KCL.

View File

@ -107,7 +107,6 @@
"check": "biome check ./src ./e2e ./packages/codemirror-lsp-client/src ./rust/kcl-language-server/client/src",
"fetch:wasm": "./scripts/get-latest-wasm-bundle.sh",
"fetch:wasm:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/get-latest-wasm-bundle.ps1",
"fetch:samples": "rm -rf public/kcl-samples* && curl -L -o public/kcl-samples.zip https://github.com/KittyCAD/kcl-samples/archive/refs/heads/achalmers/kw-args-xylineto.zip && unzip -o public/kcl-samples.zip -d public && mv public/kcl-samples-* public/kcl-samples",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js\" || echo \"sed for both mac and linux\"",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src rust/kcl-language-server/client/src",
@ -123,7 +122,6 @@
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "electron-rebuild",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js",
"tron:start": "electron-forge start",
"chrome:test": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --project='Google Chrome' --grep-invert=@snapshot",
"tronb:vite:dev": "vite build -c vite.main.config.ts -m development && vite build -c vite.preload.config.ts -m development && vite build -c vite.renderer.config.ts -m development",

View File

@ -15,10 +15,6 @@ export default function lspGoToDefinitionExt(
{
key: 'F12',
run: (view) => {
if (!plugin) {
return false
}
const value = view.plugin(plugin)
if (!value) return false

View File

@ -47,7 +47,7 @@ import {
import { isArray } from '../lib/utils'
import lspGoToDefinitionExt from './go-to-definition'
import lspRenameExt from './rename'
import lspSignatureHelpExt from './signature-help'
import { lspSignatureHelpExt, setSignatureTooltip } from './signature-help'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const docPathFacet = Facet.define<string, string>({
@ -695,6 +695,8 @@ export class LanguageServerPlugin implements PluginValue {
// Create the tooltip container
const dom = this.createTooltipContainer()
dom.className =
'documentation hover-tooltip cm-tooltip cm-signature-tooltip'
// Get active signature
const activeSignatureIndex = result.activeSignature ?? 0
@ -769,42 +771,7 @@ export class LanguageServerPlugin implements PluginValue {
)
if (tooltip) {
// Create and show the tooltip manually
const { pos: tooltipPos, create } = tooltip
const tooltipView = create(view)
const tooltipElement = document.createElement('div')
tooltipElement.className =
'documentation hover-tooltip cm-tooltip cm-signature-tooltip'
tooltipElement.style.position = 'absolute'
tooltipElement.style.zIndex = '99999999'
tooltipElement.appendChild(tooltipView.dom)
// Position the tooltip
const coords = view.coordsAtPos(tooltipPos)
if (coords) {
tooltipElement.style.left = `${coords.left}px`
tooltipElement.style.top = `${coords.bottom + 5}px`
// Add to DOM
document.body.appendChild(tooltipElement)
// Remove after a delay or on editor changes
setTimeout(() => {
removeTooltip() // Use the function that also cleans up event listeners
}, 10000) // Show for 10 seconds
// Also remove on any user input
const removeTooltip = () => {
tooltipElement.remove()
view.dom.removeEventListener('keydown', removeTooltip)
view.dom.removeEventListener('mousedown', removeTooltip)
}
view.dom.addEventListener('keydown', removeTooltip)
view.dom.addEventListener('mousedown', removeTooltip)
}
view.dispatch({ effects: setSignatureTooltip.of(tooltip) })
}
}
@ -1034,15 +1001,8 @@ export class LanguageServerPlugin implements PluginValue {
continue
}
// Sort edits in reverse order to avoid position shifts
const sortedEdits = docChange.edits.sort((a, b) => {
const posA = posToOffset(view.state.doc, a.range.start)
const posB = posToOffset(view.state.doc, b.range.start)
return (posB ?? 0) - (posA ?? 0)
})
// Create a single transaction with all changes
const changes = sortedEdits.map((edit) => ({
const changes = docChange.edits.map((edit) => ({
from: posToOffset(view.state.doc, edit.range.start) ?? 0,
to: posToOffset(view.state.doc, edit.range.end) ?? 0,
insert: edit.newText,
@ -1070,15 +1030,8 @@ export class LanguageServerPlugin implements PluginValue {
continue
}
// Sort changes in reverse order to avoid position shifts
const sortedChanges = changes.sort((a, b) => {
const posA = posToOffset(view.state.doc, a.range.start)
const posB = posToOffset(view.state.doc, b.range.start)
return (posB ?? 0) - (posA ?? 0)
})
// Create a single transaction with all changes
const changeSpecs = sortedChanges.map((change) => ({
const changeSpecs = changes.map((change) => ({
from: posToOffset(view.state.doc, change.range.start) ?? 0,
to: posToOffset(view.state.doc, change.range.end) ?? 0,
insert: change.newText,

View File

@ -15,8 +15,6 @@ export default function lspRenameExt(
{
key: 'F2',
run: (view) => {
if (!plugin) return false
const value = view.plugin(plugin)
if (!value) return false

View File

@ -1,12 +1,68 @@
import type { Extension } from '@codemirror/state'
import { Prec } from '@codemirror/state'
import type { ViewPlugin } from '@codemirror/view'
import { EditorView } from '@codemirror/view'
import { keymap } from '@codemirror/view'
import {
type EditorState,
type Extension,
Prec,
StateEffect,
StateField,
} from '@codemirror/state'
import type { Tooltip } from '@codemirror/view'
import { type ViewPlugin, showTooltip } from '@codemirror/view'
import { EditorView, keymap } from '@codemirror/view'
import { syntaxTree } from '@codemirror/language'
import { type SyntaxNode } from '@lezer/common'
import type { LanguageServerPlugin } from './lsp'
export default function lspSignatureHelpExt(
export const setSignatureTooltip = StateEffect.define<Tooltip | null>()
function findParenthesized(
state: EditorState,
pos: number,
side: 1 | 0 | -1 = 0
) {
let context: SyntaxNode | null = syntaxTree(state).resolveInner(pos, side)
while (context) {
const open = context.firstChild
if (
open &&
open.from == context.from &&
open.to == context.from + 1 &&
state.doc.sliceString(open.from, open.to) == '('
)
break
context = context.parent
}
return context
}
const signatureTooltip = StateField.define<Tooltip | null>({
create: () => null,
update(value, tr) {
for (let effect of tr.effects) {
if (effect.is(setSignatureTooltip)) return effect.value
}
if (!value) return null
if (tr.selection) {
let parens = findParenthesized(tr.state, tr.selection.main.head)
if (!parens || parens.from != value.pos) return null
}
return tr.docChanged
? { ...value, pos: tr.changes.mapPos(value.pos) }
: value
},
provide: (f) => [
showTooltip.from(f),
EditorView.domEventHandlers({
blur: (_, view) => {
if (view.state.field(f)) {
view.dispatch({ effects: setSignatureTooltip.of(null) })
}
},
}),
],
})
export function lspSignatureHelpExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [
@ -15,16 +71,16 @@ export default function lspSignatureHelpExt(
{
key: 'Mod-Shift-Space',
run: (view) => {
if (!plugin) {
return false
}
const value = view.plugin(plugin)
if (!value) return false
const pos = view.state.selection.main.head
const parens = findParenthesized(
view.state,
view.state.selection.main.head
)
if (!parens) return false
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.showSignatureHelpTooltip(view, pos)
value.showSignatureHelpTooltip(view, parens.from)
return true
},
},
@ -32,17 +88,10 @@ export default function lspSignatureHelpExt(
),
// eslint-disable-next-line @typescript-eslint/no-misused-promises
EditorView.updateListener.of(async (update) => {
if (!(plugin && update.docChanged)) return
if (!update.docChanged) return
// Make sure this is a valid user typing event.
let isRelevant = false
for (const tr of update.transactions) {
if (tr.isUserEvent('input')) {
isRelevant = true
}
}
if (!isRelevant) {
if (!update.transactions.some((tr) => tr.isUserEvent('input'))) {
// We only want signature help on user events.
return
}
@ -59,18 +108,16 @@ export default function lspSignatureHelpExt(
// Check if changes include trigger characters
const changes = update.changes
let shouldTrigger = false
let triggerPos = -1
changes.iterChanges((_fromA, _toA, _fromB, toB, inserted) => {
if (shouldTrigger) return // Skip if already found a trigger
if (triggerPos >= 0) return // Skip if already found a trigger
const text = inserted.toString()
if (!text) return
for (const char of triggerChars) {
if (text.includes(char)) {
shouldTrigger = true
triggerPos = toB
triggerCharacter = char
break
@ -78,13 +125,17 @@ export default function lspSignatureHelpExt(
}
})
if (shouldTrigger && triggerPos >= 0) {
await value.showSignatureHelpTooltip(
update.view,
triggerPos,
triggerCharacter
)
if (triggerPos >= 0) {
const parens = findParenthesized(update.view.state, triggerPos, -1)
if (parens) {
await value.showSignatureHelpTooltip(
update.view,
parens.from,
triggerCharacter
)
}
}
}),
signatureTooltip,
]
}

1
rust/Cargo.lock generated
View File

@ -1959,6 +1959,7 @@ dependencies = [
"url",
"uuid",
"validator",
"walkdir",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",

View File

@ -85,6 +85,7 @@ tynm = "0.1.10"
url = { version = "2.5.4", features = ["serde"] }
uuid = { workspace = true, features = ["v4", "v5", "js", "serde"] }
validator = { version = "0.20.0", features = ["derive"] }
walkdir = "2.5.0"
web-time = "1.1"
winnow = "=0.6.24"
zip = { workspace = true }

View File

@ -16,7 +16,7 @@ use crate::{
};
// Types with special handling.
const SPECIAL_TYPES: [&str; 5] = ["TagDeclarator", "TagIdentifier", "Start", "End", "ImportedGeometry"];
const SPECIAL_TYPES: [&str; 4] = ["TagDeclarator", "TagIdentifier", "Start", "End"];
const TYPE_REWRITES: [(&str, &str); 11] = [
("TagNode", "TagDeclarator"),

View File

@ -626,6 +626,8 @@ impl FnData {
pub(super) fn to_autocomplete_snippet(&self) -> String {
if self.name == "loft" {
return "loft([${0:sketch000}, ${1:sketch001}])".to_owned();
} else if self.name == "clone" {
return "clone(${0:part001})".to_owned();
} else if self.name == "hole" {
return "hole(${0:holeSketch}, ${1:%})".to_owned();
}

View File

@ -11,6 +11,7 @@ use std::{
use anyhow::Result;
use kcl_doc::ModData;
use parse_display::Display;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{
@ -18,15 +19,27 @@ use tower_lsp::lsp_types::{
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
};
use crate::{
execution::{types::NumericType, Sketch},
std::Primitive,
};
use crate::execution::{types::NumericType, Sketch};
// These types are declared in (KCL) std.
const DECLARED_TYPES: [&str; 15] = [
"any", "number", "string", "tag", "bool", "Sketch", "Solid", "Plane", "Helix", "Face", "Edge", "Point2d",
"Point3d", "Axis2d", "Axis3d",
const DECLARED_TYPES: [&str; 17] = [
"any",
"number",
"string",
"tag",
"bool",
"Sketch",
"Solid",
"Plane",
"Helix",
"Face",
"Edge",
"Point2d",
"Point3d",
"Axis2d",
"Axis3d",
"ImportedGeometry",
"Fn",
];
lazy_static::lazy_static! {
@ -38,6 +51,21 @@ lazy_static::lazy_static! {
};
}
/// The primitive types that can be used in a KCL file.
#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema, Display)]
#[serde(rename_all = "lowercase")]
#[display(style = "lowercase")]
enum Primitive {
/// A boolean value.
Bool,
/// A number value.
Number,
/// A string value.
String,
/// A uuid value.
Uuid,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@ -534,8 +562,6 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
fn to_autocomplete_snippet(&self) -> Result<String> {
if self.name() == "loft" {
return Ok("loft([${0:sketch000}, ${1:sketch001}])".to_string());
} else if self.name() == "clone" {
return Ok("clone(${0:part001})".to_string());
} else if self.name() == "union" {
return Ok("union([${0:extrude001}, ${1:extrude002}])".to_string());
} else if self.name() == "subtract" {
@ -702,7 +728,7 @@ pub fn get_description_string_from_schema(schema: &schemars::schema::RootSchema)
None
}
pub fn is_primitive(schema: &schemars::schema::Schema) -> Result<Option<Primitive>> {
fn is_primitive(schema: &schemars::schema::Schema) -> Result<Option<Primitive>> {
match schema {
schemars::schema::Schema::Object(o) => {
if o.enum_values.is_some() {
@ -1008,9 +1034,12 @@ mod tests {
#[test]
fn get_autocomplete_snippet_map() {
let map_fn: Box<dyn StdLibFn> = Box::new(crate::std::array::Map);
let snippet = map_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"map(${0:[0..9]})"#);
let data = kcl_doc::walk_prelude();
let DocData::Fn(map_fn) = data.find_by_name("map").unwrap() else {
panic!();
};
let snippet = map_fn.to_autocomplete_snippet();
assert_eq!(snippet, r#"map()"#);
}
#[test]
@ -1130,8 +1159,11 @@ mod tests {
#[test]
#[allow(clippy::literal_string_with_formatting_args)]
fn get_autocomplete_snippet_clone() {
let clone_fn: Box<dyn StdLibFn> = Box::new(crate::std::clone::Clone);
let snippet = clone_fn.to_autocomplete_snippet().unwrap();
let data = kcl_doc::walk_prelude();
let DocData::Fn(clone_fn) = data.find_by_name("clone").unwrap() else {
panic!();
};
let snippet = clone_fn.to_autocomplete_snippet();
assert_eq!(snippet, r#"clone(${0:part001})"#);
}

View File

@ -182,6 +182,8 @@ impl RuntimeType {
}
AstPrimitiveType::Named(name) => Self::from_alias(&name.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function => RuntimeType::Primitive(PrimitiveType::Function),
})
}
@ -363,6 +365,7 @@ pub enum PrimitiveType {
Axis2d,
Axis3d,
ImportedGeometry,
Function,
}
impl PrimitiveType {
@ -382,6 +385,7 @@ impl PrimitiveType {
PrimitiveType::Axis2d => "2d axes".to_owned(),
PrimitiveType::Axis3d => "3d axes".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
PrimitiveType::Function => "functions".to_owned(),
PrimitiveType::Tag => "tags".to_owned(),
PrimitiveType::TagId => "tag identifiers".to_owned(),
}
@ -418,6 +422,7 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
PrimitiveType::Function => write!(f, "function"),
}
}
}
@ -1184,6 +1189,10 @@ impl KclValue {
KclValue::ImportedGeometry { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::Function => match value {
KclValue::Function { .. } => Ok(value.clone()),
_ => Err(self.into()),
},
PrimitiveType::TagId => match value {
KclValue::TagIdentifier { .. } => Ok(value.clone()),
_ => Err(self.into()),
@ -1372,12 +1381,10 @@ impl KclValue {
KclValue::HomArray { ty, value, .. } => {
Some(RuntimeType::Array(Box::new(ty.clone()), ArrayLen::Known(value.len())))
}
KclValue::TagIdentifier(_) | KclValue::TagDeclarator(_) | KclValue::Uuid { .. } => {
Some(RuntimeType::Primitive(PrimitiveType::Tag))
}
KclValue::Function { .. } | KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => {
None
}
KclValue::TagIdentifier(_) => Some(RuntimeType::Primitive(PrimitiveType::TagId)),
KclValue::TagDeclarator(_) | KclValue::Uuid { .. } => Some(RuntimeType::Primitive(PrimitiveType::Tag)),
KclValue::Function { .. } => Some(RuntimeType::Primitive(PrimitiveType::Function)),
KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None,
}
}
}

View File

@ -232,6 +232,8 @@ impl PrimitiveType {
PrimitiveType::Number(suffix) => hasher.update(suffix.digestable_id()),
PrimitiveType::Boolean => hasher.update(b"bool"),
PrimitiveType::Tag => hasher.update(b"tag"),
PrimitiveType::ImportedGeometry => hasher.update(b"ImportedGeometry"),
PrimitiveType::Function => hasher.update(b"Fn"),
}
hasher.finalize().into()

View File

@ -3197,6 +3197,10 @@ pub enum PrimitiveType {
Boolean,
/// A tag.
Tag,
/// Imported from other CAD system.
ImportedGeometry,
/// `Fn`, type of functions.
Function,
/// An identifier used as a type (not really a primitive type, but whatever).
Named(Node<Identifier>),
}
@ -3210,6 +3214,8 @@ impl PrimitiveType {
("tag", None) => Some(PrimitiveType::Tag),
("number", None) => Some(PrimitiveType::Number(NumericSuffix::None)),
("number", Some(s)) => Some(PrimitiveType::Number(s)),
("ImportedGeometry", None) => Some(PrimitiveType::ImportedGeometry),
("Fn", None) => Some(PrimitiveType::Function),
_ => None,
}
}
@ -3229,6 +3235,8 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"),
PrimitiveType::Tag => write!(f, "tag"),
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
PrimitiveType::Function => write!(f, "Fn"),
PrimitiveType::Named(n) => write!(f, "{}", n.name),
}
}

View File

@ -34,14 +34,12 @@ lazy_static! {
set.insert("true", TokenType::Keyword);
set.insert("false", TokenType::Keyword);
set.insert("nil", TokenType::Keyword);
// This isn't a type because brackets are used for the type.
set.insert("array", TokenType::Keyword);
set.insert("and", TokenType::Keyword);
set.insert("or", TokenType::Keyword);
set.insert("not", TokenType::Keyword);
set.insert("var", TokenType::Keyword);
set.insert("const", TokenType::Keyword);
// "import" is special because of import().
set.insert("import", TokenType::Keyword);
set.insert("export", TokenType::Keyword);
set.insert("type", TokenType::Keyword);
set.insert("interface", TokenType::Keyword);

View File

@ -8,6 +8,7 @@ use std::{
use anyhow::Result;
use fnv::FnvHashSet;
use serde::{Deserialize, Serialize};
use walkdir::WalkDir;
use super::Test;
@ -250,19 +251,12 @@ fn get_kcl_metadata(project_path: &Path, files: &[String]) -> Option<KclMetadata
let title = lines[0].trim_start_matches(COMMENT_PREFIX).trim().to_string();
let description = lines[1].trim_start_matches(COMMENT_PREFIX).trim().to_string();
// Get the path components
let path_components: Vec<String> = full_path_to_primary_kcl
.components()
.map(|comp| comp.as_os_str().to_string_lossy().to_string())
.collect();
// Get the last two path components
let len = path_components.len();
let path_from_project_dir = if len >= 2 {
format!("{}/{}", path_components[len - 2], path_components[len - 1])
} else {
primary_kcl_file.clone()
};
// Get the relative path from the project directory to the primary KCL file
let path_from_project_dir = full_path_to_primary_kcl
.strip_prefix(INPUTS_DIR.as_path())
.unwrap_or(&full_path_to_primary_kcl)
.to_string_lossy()
.to_string();
let mut files = files.to_vec();
files.sort();
@ -281,21 +275,23 @@ fn get_kcl_metadata(project_path: &Path, files: &[String]) -> Option<KclMetadata
fn generate_kcl_manifest(dir: &Path) -> Result<()> {
let mut manifest = Vec::new();
// Collect all directory entries first and sort them by name for consistent ordering
let mut entries: Vec<_> = fs::read_dir(dir)?
.filter_map(Result::ok)
.filter(|e| e.path().is_dir())
// Collect all directory entries first
let mut entries: Vec<_> = WalkDir::new(dir)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
.collect();
// Sort directories by name for consistent ordering
entries.sort_by_key(|a| a.file_name());
entries.sort_by_key(|a| a.file_name().to_string_lossy().to_string());
// Loop through all directories and add to manifest if KCL sample
for entry in entries {
let project_path = entry.path();
let path = entry.path();
if project_path.is_dir() {
if path.is_dir() {
// Get all .kcl files in the directory
let files: Vec<String> = fs::read_dir(&project_path)?
let files: Vec<String> = fs::read_dir(path)?
.filter_map(Result::ok)
.filter(|e| {
if let Some(ext) = e.path().extension() {
@ -311,7 +307,7 @@ fn generate_kcl_manifest(dir: &Path) -> Result<()> {
continue;
}
if let Some(metadata) = get_kcl_metadata(&project_path, &files) {
if let Some(metadata) = get_kcl_metadata(path, &files) {
manifest.push(metadata);
}
}

View File

@ -1,5 +1,4 @@
use indexmap::IndexMap;
use kcl_derive_docs::stdlib;
use super::{
args::{Arg, KwArgs},
@ -27,46 +26,6 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
})
}
/// Apply a function to every element of a list.
///
/// Given a list like `[a, b, c]`, and a function like `f`, returns
/// `[f(a), f(b), f(c)]`
/// ```no_run
/// r = 10 // radius
/// fn drawCircle(@id) {
/// return startSketchOn(XY)
/// |> circle( center= [id * 2 * r, 0], radius= r)
/// }
///
/// // Call `drawCircle`, passing in each element of the array.
/// // The outputs from each `drawCircle` form a new array,
/// // which is the return value from `map`.
/// circles = map(
/// [1..3],
/// f = drawCircle
/// )
/// ```
/// ```no_run
/// r = 10 // radius
/// // Call `map`, using an anonymous function instead of a named one.
/// circles = map(
/// [1..3],
/// f = fn(@id) {
/// return startSketchOn(XY)
/// |> circle( center= [id * 2 * r, 0], radius= r)
/// }
/// )
/// ```
#[stdlib {
name = "map",
keywords = true,
unlabeled_first = true,
args = {
array = { docs = "Input array. The output array is this input array, but every element has had the function `f` run on it." },
f = { docs = "A function. The output array is just the input array, but `f` has been run on every item." },
},
tags = ["array"]
}]
async fn inner_map<'a>(
array: Vec<KclValue>,
f: &'a FunctionSource,
@ -118,96 +77,6 @@ pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
inner_reduce(array, initial, f, exec_state, &args).await
}
/// Take a starting value. Then, for each element of an array, calculate the next value,
/// using the previous value and the element.
/// ```no_run
/// // This function adds two numbers.
/// fn add(@a, accum) { return a + accum }
///
/// // This function adds an array of numbers.
/// // It uses the `reduce` function, to call the `add` function on every
/// // element of the `arr` parameter. The starting value is 0.
/// fn sum(@arr) { return reduce(arr, initial = 0, f = add) }
///
/// /*
/// The above is basically like this pseudo-code:
/// fn sum(arr):
/// sumSoFar = 0
/// for i in arr:
/// sumSoFar = add(i, sumSoFar)
/// return sumSoFar
/// */
///
/// // We use `assert` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assert(sum([1, 2, 3]), isEqualTo = 6, tolerance = 0.1, error = "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // This example works just like the previous example above, but it uses
/// // an anonymous `add` function as its parameter, instead of declaring a
/// // named function outside.
/// arr = [1, 2, 3]
/// sum = reduce(arr, initial = 0, f = fn (@i, accum) { return i + accum })
///
/// // We use `assert` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assert(sum, isEqualTo = 6, tolerance = 0.1, error = "1 + 2 + 3 summed is 6")
/// ```
/// ```no_run
/// // Declare a function that sketches a decagon.
/// fn decagon(@radius) {
/// // Each side of the decagon is turned this many radians from the previous angle.
/// stepAngle = ((1/10) * TAU): number(rad)
///
/// // Start the decagon sketch at this point.
/// startOfDecagonSketch = startSketchOn(XY)
/// |> startProfile(at = [(cos(0)*radius), (sin(0) * radius)])
///
/// // Use a `reduce` to draw the remaining decagon sides.
/// // For each number in the array 1..10, run the given function,
/// // which takes a partially-sketched decagon and adds one more edge to it.
/// fullDecagon = reduce([1..10], initial = startOfDecagonSketch, f = fn(@i, accum) {
/// // Draw one edge of the decagon.
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// return line(accum, end = [x, y])
/// })
///
/// return fullDecagon
///
/// }
///
/// /*
/// The `decagon` above is basically like this pseudo-code:
/// fn decagon(radius):
/// stepAngle = ((1/10) * TAU): number(rad)
/// plane = startSketchOn(XY)
/// startOfDecagonSketch = startProfile(plane, at = [(cos(0)*radius), (sin(0) * radius)])
///
/// // Here's the reduce part.
/// partialDecagon = startOfDecagonSketch
/// for i in [1..10]:
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// partialDecagon = line(partialDecagon, end = [x, y])
/// fullDecagon = partialDecagon // it's now full
/// return fullDecagon
/// */
///
/// // Use the `decagon` function declared above, to sketch a decagon with radius 5.
/// decagon(5.0) |> close()
/// ```
#[stdlib {
name = "reduce",
keywords = true,
unlabeled_first = true,
args = {
array = { docs = "Each element of this array gets run through the function `f`, combined with the previous output from `f`, and then used for the next run." },
initial = { docs = "The first time `f` is run, it will be called with the first item of `array` and this initial starting value."},
f = { docs = "Run once per item in the input `array`. This function takes an item from the array, and the previous output from `f` (or `initial` on the very first run). The final time `f` is run, its output is returned as the final output from `reduce`." },
},
tags = ["array"]
}]
async fn inner_reduce<'a>(
array: Vec<KclValue>,
initial: KclValue,
@ -285,28 +154,8 @@ pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
Ok(KclValue::HomArray { value: new_array, ty })
}
/// Append an element to the end of an array.
///
/// Returns a new array with the element appended.
///
/// ```no_run
/// arr = [1, 2, 3]
/// new_arr = push(arr, item = 4)
/// assert(new_arr[3], isEqualTo = 4, tolerance = 0.1, error = "4 was added to the end of the array")
/// ```
#[stdlib {
name = "push",
keywords = true,
unlabeled_first = true,
args = {
array = { docs = "The array which you're adding a new item to." },
item = { docs = "The new item to add to the array" },
},
tags = ["array"]
}]
fn inner_push(mut array: Vec<KclValue>, item: KclValue) -> Vec<KclValue> {
array.push(item);
array
}
@ -322,30 +171,9 @@ pub async fn pop(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
};
let new_array = inner_pop(values, &args)?;
Ok(KclValue::HomArray { value: new_array, ty })
}
/// Remove the last element from an array.
///
/// Returns a new array with the last element removed.
///
/// ```no_run
/// arr = [1, 2, 3, 4]
/// new_arr = pop(arr)
/// assert(new_arr[0], isEqualTo = 1, tolerance = 0.00001, error = "1 is the first element of the array")
/// assert(new_arr[1], isEqualTo = 2, tolerance = 0.00001, error = "2 is the second element of the array")
/// assert(new_arr[2], isEqualTo = 3, tolerance = 0.00001, error = "3 is the third element of the array")
/// ```
#[stdlib {
name = "pop",
keywords = true,
unlabeled_first = true,
args = {
array = { docs = "The array to pop from. Must not be empty."},
},
tags = ["array"]
}]
fn inner_pop(array: Vec<KclValue>, args: &Args) -> Result<Vec<KclValue>, KclError> {
if array.is_empty() {
return Err(KclError::Semantic(KclErrorDetails {

View File

@ -3,7 +3,6 @@
use std::collections::HashMap;
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd,
ok_response::{output::EntityGetAllChildUuids, OkModelingCmdResponse},
@ -41,240 +40,6 @@ pub async fn clone(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
Ok(cloned.into())
}
/// Clone a sketch or solid.
///
/// This works essentially like a copy-paste operation. It creates a perfect replica
/// at that point in time that you can manipulate individually afterwards.
///
/// This doesn't really have much utility unless you need the equivalent of a double
/// instance pattern with zero transformations.
///
/// Really only use this function if YOU ARE SURE you need it. In most cases you
/// do not need clone and using a pattern with `instance = 2` is more appropriate.
///
/// ```no_run
/// // Clone a basic sketch and move it and extrude it.
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// clonedSketch = clone(exampleSketch)
/// |> scale(
/// x = 1.0,
/// y = 1.0,
/// z = 2.5,
/// )
/// |> translate(
/// x = 15.0,
/// y = 0,
/// z = 0,
/// )
/// |> extrude(length = 5)
/// ```
///
/// ```no_run
/// // Clone a basic solid and move it.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// myPart = extrude(exampleSketch, length = 5)
/// clonedPart = clone(myPart)
/// |> translate(
/// x = 25.0,
/// )
/// ```
///
/// ```no_run
/// // Translate and rotate a cloned sketch to create a loft.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [-10, 10])
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20)
/// |> close()
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 0, y = 0, z = 20)
/// |> rotate(axis = [0, 0, 1.0], angle = 45)
///
/// loft([sketch001, sketch002])
/// ```
///
/// ```no_run
/// // Translate a cloned solid. Fillet only the clone.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [-10, 10])
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20, tag = $filletTag)
/// |> close()
/// |> extrude(length = 5)
///
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 0, y = 0, z = 20)
/// |> fillet(
/// radius = 2,
/// tags = [getNextAdjacentEdge(filletTag)],
/// )
/// ```
///
/// ```no_run
/// // You can reuse the tags from the original geometry with the cloned geometry.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10], tag = $sketchingFace)
/// |> line(end = [-10, 0])
/// |> close()
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 10, y = 20, z = 0)
/// |> extrude(length = 5)
///
/// startSketchOn(sketch002, face = sketchingFace)
/// |> startProfile(at = [1, 1])
/// |> line(end = [8, 0])
/// |> line(end = [0, 8])
/// |> line(end = [-8, 0])
/// |> close(tag = $sketchingFace002)
/// |> extrude(length = 10)
/// ```
///
/// ```no_run
/// // You can also use the tags from the original geometry to fillet the cloned geometry.
///
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 2
///
/// mountingPlateSketch = startSketchOn(XY)
/// |> startProfile(at = [-width/2, -length/2])
/// |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
/// |> line(endAbsolute = [width/2, length/2], tag = $edge2)
/// |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
/// |> close(tag = $edge4)
///
/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
///
/// clonedMountingPlate = clone(mountingPlate)
/// |> fillet(
/// radius = filletRadius,
/// tags = [
/// getNextAdjacentEdge(edge1),
/// getNextAdjacentEdge(edge2),
/// getNextAdjacentEdge(edge3),
/// getNextAdjacentEdge(edge4)
/// ],
/// )
/// |> translate(x = 0, y = 50, z = 0)
/// ```
///
/// ```no_run
/// // Create a spring by sweeping around a helix path from a cloned sketch.
///
/// // Create a helix around the Z axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 4,
/// length = 10,
/// radius = 5,
/// axis = Z,
/// )
///
///
/// springSketch = startSketchOn(YZ)
/// |> circle( center = [0, 0], radius = 1)
///
/// // Create a spring by sweeping around the helix path.
/// sweepedSpring = clone(springSketch)
/// |> translate(x=100)
/// |> sweep(path = helixPath)
/// ```
///
/// ```
/// // A donut shape from a cloned sketch.
/// sketch001 = startSketchOn(XY)
/// |> circle( center = [15, 0], radius = 5 )
///
/// sketch002 = clone(sketch001)
/// |> translate( z = 30)
/// |> revolve(
/// angle = 360,
/// axis = Y,
/// )
/// ```
///
/// ```no_run
/// // Sketch on the end of a revolved face by tagging the end face.
/// // This shows the cloned geometry will have the same tags as the original geometry.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [4, 12])
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
///
/// example001 = revolve(exampleSketch, axis = Y, angle = 180, tagEnd = $end01)
///
/// // example002 = clone(example001)
/// // |> translate(x = 0, y = 20, z = 0)
///
/// // Sketch on the cloned face.
/// // exampleSketch002 = startSketchOn(example002, face = end01)
/// // |> startProfile(at = [4.5, -5])
/// // |> line(end = [0, 5])
/// // |> line(end = [5, 0])
/// // |> line(end = [0, -5])
/// // |> close()
///
/// // example003 = extrude(exampleSketch002, length = 5)
/// ```
///
/// ```no_run
/// // Clone an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// myCube = cube
///
/// clonedCube = clone(myCube)
/// |> translate(
/// x = 1020,
/// )
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
#[stdlib {
name = "clone",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
geometry = { docs = "The sketch, solid, or imported geometry to be cloned" },
}
}]
async fn inner_clone(
geometry: GeometryWithImportedGeometry,
exec_state: &mut ExecState,

View File

@ -231,3 +231,38 @@ pub async fn ln(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclE
Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
}
/// Compute the length of the given leg.
pub async fn leg_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = (hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt();
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
}
/// Compute the angle of the given leg for x.
pub async fn leg_angle_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = (leg.min(hypotenuse) / hypotenuse).acos().to_degrees();
Ok(KclValue::from_number_with_type(
result,
NumericType::degrees(),
vec![args.into()],
))
}
/// Compute the angle of the given leg for y.
pub async fn leg_angle_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = (leg.min(hypotenuse) / hypotenuse).asin().to_degrees();
Ok(KclValue::from_number_with_type(
result,
NumericType::degrees(),
vec![args.into()],
))
}

View File

@ -28,21 +28,13 @@ pub mod utils;
use anyhow::Result;
pub use args::Args;
use args::TyF64;
use indexmap::IndexMap;
use kcl_derive_docs::stdlib;
use lazy_static::lazy_static;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
docs::StdLibFn,
errors::KclError,
execution::{
types::{NumericType, PrimitiveType, RuntimeType, UnitAngle, UnitType},
ExecState, KclValue,
},
execution::{types::PrimitiveType, ExecState, KclValue},
parsing::ast::types::Name,
};
@ -53,9 +45,6 @@ pub type StdFn = fn(
lazy_static! {
static ref CORE_FNS: Vec<Box<dyn StdLibFn>> = vec![
Box::new(LegLen),
Box::new(LegAngX),
Box::new(LegAngY),
Box::new(crate::std::appearance::Appearance),
Box::new(crate::std::extrude::Extrude),
Box::new(crate::std::segment::SegEnd),
@ -87,17 +76,12 @@ lazy_static! {
Box::new(crate::std::sketch::TangentialArc),
Box::new(crate::std::sketch::BezierCurve),
Box::new(crate::std::sketch::Subtract2D),
Box::new(crate::std::clone::Clone),
Box::new(crate::std::patterns::PatternLinear2D),
Box::new(crate::std::patterns::PatternLinear3D),
Box::new(crate::std::patterns::PatternCircular2D),
Box::new(crate::std::patterns::PatternCircular3D),
Box::new(crate::std::patterns::PatternTransform),
Box::new(crate::std::patterns::PatternTransform2D),
Box::new(crate::std::array::Reduce),
Box::new(crate::std::array::Map),
Box::new(crate::std::array::Push),
Box::new(crate::std::array::Pop),
Box::new(crate::std::edge::GetOppositeEdge),
Box::new(crate::std::edge::GetNextAdjacentEdge),
Box::new(crate::std::edge::GetPreviousAdjacentEdge),
@ -149,84 +133,96 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
match (path, fn_name) {
("math", "cos") => (
|e, a| Box::pin(crate::std::math::cos(e, a)),
StdFnProps::default("std::cos"),
StdFnProps::default("std::math::cos"),
),
("math", "sin") => (
|e, a| Box::pin(crate::std::math::sin(e, a)),
StdFnProps::default("std::sin"),
StdFnProps::default("std::math::sin"),
),
("math", "tan") => (
|e, a| Box::pin(crate::std::math::tan(e, a)),
StdFnProps::default("std::tan"),
StdFnProps::default("std::math::tan"),
),
("math", "acos") => (
|e, a| Box::pin(crate::std::math::acos(e, a)),
StdFnProps::default("std::acos"),
StdFnProps::default("std::math::acos"),
),
("math", "asin") => (
|e, a| Box::pin(crate::std::math::asin(e, a)),
StdFnProps::default("std::asin"),
StdFnProps::default("std::math::asin"),
),
("math", "atan") => (
|e, a| Box::pin(crate::std::math::atan(e, a)),
StdFnProps::default("std::atan"),
StdFnProps::default("std::math::atan"),
),
("math", "atan2") => (
|e, a| Box::pin(crate::std::math::atan2(e, a)),
StdFnProps::default("std::atan2"),
StdFnProps::default("std::math::atan2"),
),
("math", "sqrt") => (
|e, a| Box::pin(crate::std::math::sqrt(e, a)),
StdFnProps::default("std::sqrt"),
StdFnProps::default("std::math::sqrt"),
),
("math", "abs") => (
|e, a| Box::pin(crate::std::math::abs(e, a)),
StdFnProps::default("std::abs"),
StdFnProps::default("std::math::abs"),
),
("math", "rem") => (
|e, a| Box::pin(crate::std::math::rem(e, a)),
StdFnProps::default("std::rem"),
StdFnProps::default("std::math::rem"),
),
("math", "round") => (
|e, a| Box::pin(crate::std::math::round(e, a)),
StdFnProps::default("std::round"),
StdFnProps::default("std::math::round"),
),
("math", "floor") => (
|e, a| Box::pin(crate::std::math::floor(e, a)),
StdFnProps::default("std::floor"),
StdFnProps::default("std::math::floor"),
),
("math", "ceil") => (
|e, a| Box::pin(crate::std::math::ceil(e, a)),
StdFnProps::default("std::ceil"),
StdFnProps::default("std::math::ceil"),
),
("math", "min") => (
|e, a| Box::pin(crate::std::math::min(e, a)),
StdFnProps::default("std::min"),
StdFnProps::default("std::math::min"),
),
("math", "max") => (
|e, a| Box::pin(crate::std::math::max(e, a)),
StdFnProps::default("std::max"),
StdFnProps::default("std::math::max"),
),
("math", "pow") => (
|e, a| Box::pin(crate::std::math::pow(e, a)),
StdFnProps::default("std::pow"),
StdFnProps::default("std::math::pow"),
),
("math", "log") => (
|e, a| Box::pin(crate::std::math::log(e, a)),
StdFnProps::default("std::log"),
StdFnProps::default("std::math::log"),
),
("math", "log2") => (
|e, a| Box::pin(crate::std::math::log2(e, a)),
StdFnProps::default("std::log2"),
StdFnProps::default("std::math::log2"),
),
("math", "log10") => (
|e, a| Box::pin(crate::std::math::log10(e, a)),
StdFnProps::default("std::log10"),
StdFnProps::default("std::math::log10"),
),
("math", "ln") => (
|e, a| Box::pin(crate::std::math::ln(e, a)),
StdFnProps::default("std::ln"),
StdFnProps::default("std::math::ln"),
),
("math", "legLen") => (
|e, a| Box::pin(crate::std::math::leg_length(e, a)),
StdFnProps::default("std::math::legLen"),
),
("math", "legAngX") => (
|e, a| Box::pin(crate::std::math::leg_angle_x(e, a)),
StdFnProps::default("std::math::legAngX"),
),
("math", "legAngY") => (
|e, a| Box::pin(crate::std::math::leg_angle_y(e, a)),
StdFnProps::default("std::math::legAngY"),
),
("sketch", "circle") => (
|e, a| Box::pin(crate::std::shapes::circle(e, a)),
@ -264,6 +260,26 @@ pub(crate) fn std_fn(path: &str, fn_name: &str) -> (crate::std::StdFn, StdFnProp
|e, a| Box::pin(crate::std::shell::hollow(e, a)),
StdFnProps::default("std::solid::hollow").include_in_feature_tree(),
),
("array", "map") => (
|e, a| Box::pin(crate::std::array::map(e, a)),
StdFnProps::default("std::array::map"),
),
("array", "reduce") => (
|e, a| Box::pin(crate::std::array::reduce(e, a)),
StdFnProps::default("std::array::reduce"),
),
("array", "push") => (
|e, a| Box::pin(crate::std::array::push(e, a)),
StdFnProps::default("std::array::push"),
),
("array", "pop") => (
|e, a| Box::pin(crate::std::array::pop(e, a)),
StdFnProps::default("std::array::pop"),
),
("prelude", "clone") => (
|e, a| Box::pin(crate::std::clone::clone(e, a)),
StdFnProps::default("std::clone").include_in_feature_tree(),
),
_ => unreachable!(),
}
}
@ -341,110 +357,3 @@ pub enum FunctionKind {
/// The default tolerance for modeling commands in [`kittycad_modeling_cmds::length_unit::LengthUnit`].
const DEFAULT_TOLERANCE: f64 = 0.0000001;
/// Compute the length of the given leg.
pub async fn leg_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = inner_leg_length(hypotenuse, leg);
Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
}
/// Compute the length of the given leg.
///
/// ```kcl,no_run
/// legLen(hypotenuse = 5, leg = 3)
/// ```
#[stdlib {
name = "legLen",
keywords = true,
unlabeled_first = false,
args = {
hypotenuse = { docs = "The length of the triangle's hypotenuse" },
leg = { docs = "The length of one of the triangle's legs (i.e. non-hypotenuse side)" },
},
tags = ["math"],
}]
fn inner_leg_length(hypotenuse: f64, leg: f64) -> f64 {
(hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt()
}
/// Compute the angle of the given leg for x.
pub async fn leg_angle_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = inner_leg_angle_x(hypotenuse, leg);
Ok(KclValue::from_number_with_type(
result,
NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
vec![args.into()],
))
}
/// Compute the angle of the given leg for x.
///
/// ```kcl,no_run
/// legAngX(hypotenuse = 5, leg = 3)
/// ```
#[stdlib {
name = "legAngX",
keywords = true,
unlabeled_first = false,
args = {
hypotenuse = { docs = "The length of the triangle's hypotenuse" },
leg = { docs = "The length of one of the triangle's legs (i.e. non-hypotenuse side)" },
},
tags = ["math"],
}]
fn inner_leg_angle_x(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).acos().to_degrees()
}
/// Compute the angle of the given leg for y.
pub async fn leg_angle_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let hypotenuse: TyF64 = args.get_kw_arg_typed("hypotenuse", &RuntimeType::length(), exec_state)?;
let leg: TyF64 = args.get_kw_arg_typed("leg", &RuntimeType::length(), exec_state)?;
let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg);
let result = inner_leg_angle_y(hypotenuse, leg);
Ok(KclValue::from_number_with_type(
result,
NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
vec![args.into()],
))
}
/// Compute the angle of the given leg for y.
///
/// ```kcl,no_run
/// legAngY(hypotenuse = 5, leg = 3)
/// ```
#[stdlib {
name = "legAngY",
keywords = true,
unlabeled_first = false,
args = {
hypotenuse = { docs = "The length of the triangle's hypotenuse" },
leg = { docs = "The length of one of the triangle's legs (i.e. non-hypotenuse side)" },
},
tags = ["math"],
}]
fn inner_leg_angle_y(hypotenuse: f64, leg: f64) -> f64 {
(leg.min(hypotenuse) / hypotenuse).asin().to_degrees()
}
/// The primitive types that can be used in a KCL file.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Display, FromStr)]
#[serde(rename_all = "lowercase")]
#[display(style = "lowercase")]
pub enum Primitive {
/// A boolean value.
Bool,
/// A number value.
Number,
/// A string value.
String,
/// A uuid value.
Uuid,
}

View File

@ -2,3 +2,169 @@
@no_std
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
/// Apply a function to every element of a list.
///
/// Given a list like `[a, b, c]`, and a function like `f`, returns
/// `[f(a), f(b), f(c)]`
///
/// ```kcl
/// r = 10 // radius
/// fn drawCircle(@id) {
/// return startSketchOn(XY)
/// |> circle( center= [id * 2 * r, 0], radius= r)
/// }
///
/// // Call `drawCircle`, passing in each element of the array.
/// // The outputs from each `drawCircle` form a new array,
/// // which is the return value from `map`.
/// circles = map(
/// [1..3],
/// f = drawCircle
/// )
/// ```
///
/// ```kcl
/// r = 10 // radius
/// // Call `map`, using an anonymous function instead of a named one.
/// circles = map(
/// [1..3],
/// f = fn(@id) {
/// return startSketchOn(XY)
/// |> circle( center= [id * 2 * r, 0], radius= r)
/// }
/// )
/// ```
@(impl = std_rust)
export fn map(
/// Input array. The output array is this input array, but every element has had the function `f` run on it.
@array: [any],
/// A function. The output array is just the input array, but `f` has been run on every item.
f: Fn,
): [any] {}
/// Take a starting value. Then, for each element of an array, calculate the next value,
/// using the previous value and the element.
///
/// ```kcl
/// // This function adds two numbers.
/// fn add(@a, accum) { return a + accum }
///
/// // This function adds an array of numbers.
/// // It uses the `reduce` function, to call the `add` function on every
/// // element of the `arr` parameter. The starting value is 0.
/// fn sum(@arr) { return reduce(arr, initial = 0, f = add) }
///
/// /*
/// The above is basically like this pseudo-code:
/// fn sum(arr):
/// sumSoFar = 0
/// for i in arr:
/// sumSoFar = add(i, sumSoFar)
/// return sumSoFar
/// */
///
/// // We use `assert` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assert(sum([1, 2, 3]), isEqualTo = 6, tolerance = 0.1, error = "1 + 2 + 3 summed is 6")
/// ```
///
/// ```kcl
/// // This example works just like the previous example above, but it uses
/// // an anonymous `add` function as its parameter, instead of declaring a
/// // named function outside.
/// arr = [1, 2, 3]
/// sum = reduce(arr, initial = 0, f = fn (@i, accum) { return i + accum })
///
/// // We use `assert` to check that our `sum` function gives the
/// // expected result. It's good to check your work!
/// assert(sum, isEqualTo = 6, tolerance = 0.1, error = "1 + 2 + 3 summed is 6")
/// ```
///
/// ```kcl
/// // Declare a function that sketches a decagon.
/// fn decagon(@radius) {
/// // Each side of the decagon is turned this many radians from the previous angle.
/// stepAngle = ((1/10) * TAU): number(rad)
///
/// // Start the decagon sketch at this point.
/// startOfDecagonSketch = startSketchOn(XY)
/// |> startProfile(at = [(cos(0)*radius), (sin(0) * radius)])
///
/// // Use a `reduce` to draw the remaining decagon sides.
/// // For each number in the array 1..10, run the given function,
/// // which takes a partially-sketched decagon and adds one more edge to it.
/// fullDecagon = reduce([1..10], initial = startOfDecagonSketch, f = fn(@i, accum) {
/// // Draw one edge of the decagon.
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// return line(accum, end = [x, y])
/// })
///
/// return fullDecagon
///
/// }
///
/// /*
/// The `decagon` above is basically like this pseudo-code:
/// fn decagon(radius):
/// stepAngle = ((1/10) * TAU): number(rad)
/// plane = startSketchOn(XY)
/// startOfDecagonSketch = startProfile(plane, at = [(cos(0)*radius), (sin(0) * radius)])
///
/// // Here's the reduce part.
/// partialDecagon = startOfDecagonSketch
/// for i in [1..10]:
/// x = cos(stepAngle * i) * radius
/// y = sin(stepAngle * i) * radius
/// partialDecagon = line(partialDecagon, end = [x, y])
/// fullDecagon = partialDecagon // it's now full
/// return fullDecagon
/// */
///
/// // Use the `decagon` function declared above, to sketch a decagon with radius 5.
/// decagon(5.0) |> close()
/// ```
@(impl = std_rust)
export fn reduce(
/// Each element of this array gets run through the function `f`, combined with the previous output from `f`, and then used for the next run.
@array: [any],
/// The first time `f` is run, it will be called with the first item of `array` and this initial starting value.
initial: any,
/// Run once per item in the input `array`. This function takes an item from the array, and the previous output from `f` (or `initial` on the very first run). The final time `f` is run, its output is returned as the final output from `reduce`.
f: Fn,
): [any] {}
/// Append an element to the end of an array.
///
/// Returns a new array with the element appended.
///
/// ```kcl
/// arr = [1, 2, 3]
/// new_arr = push(arr, item = 4)
/// assert(new_arr[3], isEqualTo = 4, tolerance = 0.1, error = "4 was added to the end of the array")
/// ```
@(impl = std_rust)
export fn push(
/// The array which you're adding a new item to.
@array: [any],
/// The new item to add to the array
item: any,
): [any; 1+] {}
/// Remove the last element from an array.
///
/// Returns a new array with the last element removed.
///
/// ```kcl
/// arr = [1, 2, 3, 4]
/// new_arr = pop(arr)
/// assert(new_arr[0], isEqualTo = 1, tolerance = 0.00001, error = "1 is the first element of the array")
/// assert(new_arr[1], isEqualTo = 2, tolerance = 0.00001, error = "2 is the second element of the array")
/// assert(new_arr[2], isEqualTo = 3, tolerance = 0.00001, error = "3 is the third element of the array")
/// ```
@(impl = std_rust)
export fn pop(
/// The array to pop from. Must not be empty.
@array: [any; 1+],
): [any] {}

View File

@ -436,3 +436,42 @@ export fn log10(@input: number): number {}
/// ```
@(impl = std_rust)
export fn ln(@input: number): number {}
/// Compute the length of the given leg.
///
/// ```kcl,no_run
/// legLen(hypotenuse = 5, leg = 3)
/// ```
@(impl = std_rust)
export fn legLen(
/// The length of the triangle's hypotenuse.
hypotenuse: number(Length),
/// The length of one of the triangle's legs (i.e. non-hypotenuse side).
leg: number(Length),
): number(deg) {}
/// Compute the angle of the given leg for x.
///
/// ```kcl,no_run
/// legAngX(hypotenuse = 5, leg = 3)
/// ```
@(impl = std_rust)
export fn legAngX(
/// The length of the triangle's hypotenuse.
hypotenuse: number(Length),
/// The length of one of the triangle's legs (i.e. non-hypotenuse side).
leg: number(Length),
): number(deg) {}
/// Compute the angle of the given leg for y.
///
/// ```kcl,no_run
/// legAngY(hypotenuse = 5, leg = 3)
/// ```
@(impl = std_rust)
export fn legAngY(
/// The length of the triangle's hypotenuse.
hypotenuse: number(Length),
/// The length of one of the triangle's legs (i.e. non-hypotenuse side).
leg: number(Length),
): number(deg) {}

View File

@ -251,3 +251,234 @@ export fn offsetPlane(
/// Distance from the standard plane this new plane will be created at.
offset: number(Length),
): Plane {}
/// Clone a sketch or solid.
///
/// This works essentially like a copy-paste operation. It creates a perfect replica
/// at that point in time that you can manipulate individually afterwards.
///
/// This doesn't really have much utility unless you need the equivalent of a double
/// instance pattern with zero transformations.
///
/// Really only use this function if YOU ARE SURE you need it. In most cases you
/// do not need clone and using a pattern with `instance = 2` is more appropriate.
///
/// ```kcl
/// // Clone a basic sketch and move it and extrude it.
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// clonedSketch = clone(exampleSketch)
/// |> scale(
/// x = 1.0,
/// y = 1.0,
/// z = 2.5,
/// )
/// |> translate(
/// x = 15.0,
/// y = 0,
/// z = 0,
/// )
/// |> extrude(length = 5)
/// ```
///
/// ```kcl
/// // Clone a basic solid and move it.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10])
/// |> line(end = [-10, 0])
/// |> close()
///
/// myPart = extrude(exampleSketch, length = 5)
/// clonedPart = clone(myPart)
/// |> translate(
/// x = 25.0,
/// )
/// ```
///
/// ```kcl
/// // Translate and rotate a cloned sketch to create a loft.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [-10, 10])
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20)
/// |> close()
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 0, y = 0, z = 20)
/// |> rotate(axis = [0, 0, 1.0], angle = 45)
///
/// loft([sketch001, sketch002])
/// ```
///
/// ```kcl
/// // Translate a cloned solid. Fillet only the clone.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [-10, 10])
/// |> xLine(length = 20)
/// |> yLine(length = -20)
/// |> xLine(length = -20, tag = $filletTag)
/// |> close()
/// |> extrude(length = 5)
///
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 0, y = 0, z = 20)
/// |> fillet(
/// radius = 2,
/// tags = [getNextAdjacentEdge(filletTag)],
/// )
/// ```
///
/// ```kcl
/// // You can reuse the tags from the original geometry with the cloned geometry.
///
/// sketch001 = startSketchOn(XY)
/// |> startProfile(at = [0, 0])
/// |> line(end = [10, 0])
/// |> line(end = [0, 10], tag = $sketchingFace)
/// |> line(end = [-10, 0])
/// |> close()
///
/// sketch002 = clone(sketch001)
/// |> translate(x = 10, y = 20, z = 0)
/// |> extrude(length = 5)
///
/// startSketchOn(sketch002, face = sketchingFace)
/// |> startProfile(at = [1, 1])
/// |> line(end = [8, 0])
/// |> line(end = [0, 8])
/// |> line(end = [-8, 0])
/// |> close(tag = $sketchingFace002)
/// |> extrude(length = 10)
/// ```
///
/// ```kcl
/// // You can also use the tags from the original geometry to fillet the cloned geometry.
///
/// width = 20
/// length = 10
/// thickness = 1
/// filletRadius = 2
///
/// mountingPlateSketch = startSketchOn(XY)
/// |> startProfile(at = [-width/2, -length/2])
/// |> line(endAbsolute = [width/2, -length/2], tag = $edge1)
/// |> line(endAbsolute = [width/2, length/2], tag = $edge2)
/// |> line(endAbsolute = [-width/2, length/2], tag = $edge3)
/// |> close(tag = $edge4)
///
/// mountingPlate = extrude(mountingPlateSketch, length = thickness)
///
/// clonedMountingPlate = clone(mountingPlate)
/// |> fillet(
/// radius = filletRadius,
/// tags = [
/// getNextAdjacentEdge(edge1),
/// getNextAdjacentEdge(edge2),
/// getNextAdjacentEdge(edge3),
/// getNextAdjacentEdge(edge4)
/// ],
/// )
/// |> translate(x = 0, y = 50, z = 0)
/// ```
///
/// ```kcl
/// // Create a spring by sweeping around a helix path from a cloned sketch.
///
/// // Create a helix around the Z axis.
/// helixPath = helix(
/// angleStart = 0,
/// ccw = true,
/// revolutions = 4,
/// length = 10,
/// radius = 5,
/// axis = Z,
/// )
///
///
/// springSketch = startSketchOn(YZ)
/// |> circle( center = [0, 0], radius = 1)
///
/// // Create a spring by sweeping around the helix path.
/// sweepedSpring = clone(springSketch)
/// |> translate(x=100)
/// |> sweep(path = helixPath)
/// ```
///
/// ```kcl
/// // A donut shape from a cloned sketch.
/// sketch001 = startSketchOn(XY)
/// |> circle( center = [15, 0], radius = 5 )
///
/// sketch002 = clone(sketch001)
/// |> translate( z = 30)
/// |> revolve(
/// angle = 360,
/// axis = Y,
/// )
/// ```
///
/// ```kcl
/// // Sketch on the end of a revolved face by tagging the end face.
/// // This shows the cloned geometry will have the same tags as the original geometry.
///
/// exampleSketch = startSketchOn(XY)
/// |> startProfile(at = [4, 12])
/// |> line(end = [2, 0])
/// |> line(end = [0, -6])
/// |> line(end = [4, -6])
/// |> line(end = [0, -6])
/// |> line(end = [-3.75, -4.5])
/// |> line(end = [0, -5.5])
/// |> line(end = [-2, 0])
/// |> close()
///
/// example001 = revolve(exampleSketch, axis = Y, angle = 180, tagEnd = $end01)
///
/// // example002 = clone(example001)
/// // |> translate(x = 0, y = 20, z = 0)
///
/// // Sketch on the cloned face.
/// // exampleSketch002 = startSketchOn(example002, face = end01)
/// // |> startProfile(at = [4.5, -5])
/// // |> line(end = [0, 5])
/// // |> line(end = [5, 0])
/// // |> line(end = [0, -5])
/// // |> close()
///
/// // example003 = extrude(exampleSketch002, length = 5)
/// ```
///
/// ```kcl
/// // Clone an imported model.
///
/// import "tests/inputs/cube.sldprt" as cube
///
/// myCube = cube
///
/// clonedCube = clone(myCube)
/// |> translate(
/// x = 1020,
/// )
/// |> appearance(
/// color = "#ff0000",
/// metalness = 50,
/// roughness = 50
/// )
/// ```
@(impl = std_rust)
export fn clone(
/// The sketch, solid, or imported geometry to be cloned.
@geometry: Sketch | Solid | ImportedGeometry,
): Sketch | Solid | ImportedGeometry {}

View File

@ -163,6 +163,14 @@ export type string
@(impl = primitive)
export type tag
/// Represents geometry which is defined using some other CAD system and imported into KCL.
@(impl = primitive)
export type ImportedGeometry
/// The type of any function in KCL.
@(impl = primitive)
export type Fn
/// An abstract plane.
///
/// A plane has a position and orientation in space defined by its origin and axes. A plane is abstract

View File

@ -4,11 +4,20 @@ description: Error from executing argument_error.kcl
---
KCL Semantic error
× semantic: This function expected the input argument to be of type
│ Vec<KclValue> but it's actually of type Function
╭─[5:5]
× semantic: f requires a value with type `Fn`, but found array (list)
╭─[5:1]
4 │
5 │ map(f, f = [0, 1])
·
· ╰── tests/argument_error/input.kcl
· ─────────┬────────
· ╰── tests/argument_error/input.kcl
· ╰── tests/argument_error/input.kcl
╰────
╰─▶ KCL Semantic error
× semantic: f requires a value with type `Fn`, but found array (list)
╭─[5:12]
4 │
5 │ map(f, f = [0, 1])
· ───┬──
· ╰── tests/argument_error/input.kcl
╰────

View File

@ -4,10 +4,22 @@ description: Error from executing array_elem_pop_empty_fail.kcl
---
KCL Semantic error
× semantic: Cannot pop from an empty array
× semantic: The input argument of `std::array::pop` requires a value with
│ type `[any; 1+]`, but found array (list)
╭─[2:8]
1 │ arr = []
2 │ fail = pop(arr)
· ────┬───
· ────┬───
· │ ╰── tests/array_elem_pop_empty_fail/input.kcl
· ╰── tests/array_elem_pop_empty_fail/input.kcl
╰────
╰─▶ KCL Semantic error
× semantic: The input argument of `std::array::pop` requires a value
│ with type `[any; 1+]`, but found array (list)
╭─[2:12]
1 │ arr = []
2 │ fail = pop(arr)
· ─┬─
· ╰── tests/array_elem_pop_empty_fail/input.kcl
╰────

View File

@ -105,10 +105,8 @@ description: Operations executed clone_w_fillets.kcl
"sourceRange": []
},
{
"labeledArgs": {},
"type": "KclStdLibCall",
"name": "clone",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Solid",
@ -117,6 +115,8 @@ description: Operations executed clone_w_fillets.kcl
}
},
"sourceRange": []
}
},
"labeledArgs": {},
"sourceRange": []
}
]

View File

@ -98,10 +98,8 @@ description: Operations executed clone_w_shell.kcl
"sourceRange": []
},
{
"labeledArgs": {},
"type": "KclStdLibCall",
"name": "clone",
"sourceRange": [],
"type": "StdLibCall",
"unlabeledArg": {
"value": {
"type": "Solid",
@ -110,6 +108,8 @@ description: Operations executed clone_w_shell.kcl
}
},
"sourceRange": []
}
},
"labeledArgs": {},
"sourceRange": []
}
]

View File

@ -678,7 +678,7 @@ flowchart LR
99 --- 245
99 --- 293
106 --- 193
106 x--> 206
106 x--> 207
106 --- 272
106 --- 321
126 --- 194
@ -1012,7 +1012,7 @@ flowchart LR
233 <--x 205
234 <--x 205
235 <--x 205
272 <--x 207
272 <--x 206
254 <--x 213
255 <--x 213
256 <--x 213

View File

@ -709,6 +709,7 @@ flowchart LR
115 --- 179
115 x--> 228
115 --- 256
115 x--> 307
115 --- 308
164 <--x 116
116 --- 184

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@ -1,6 +1,7 @@
#!/bin/bash
export COMMIT=$(git rev-parse --short HEAD)
COMMIT=$(git rev-parse --short HEAD)
TITLE=$(git show -s --format=%s $COMMIT)
# package.json
PACKAGE=$(jq '.productName="Zoo Design Studio (Nightly)" | .name="zoo-modeling-app-nightly"' package.json --indent 2)
@ -12,7 +13,7 @@ yq -i '.appId = "dev.zoo.modeling-app-nightly"' electron-builder.yml
yq -i '.nsis.include = "./scripts/installer-nightly.nsh"' electron-builder.yml
# Release notes
echo "Nightly build (commit $COMMIT)" > release-notes.md
echo "[$TITLE](https://github.com/KittyCAD/modeling-app/commit/$COMMIT)" > release-notes.md
# icons
cp assets/icon-nightly.png assets/icon.png

View File

@ -1,5 +0,0 @@
#!/bin/bash
echo "## What's Changed"
git log ${PREVIOUS_TAG}..HEAD --oneline --pretty=format:%s | grep -v Bump | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
echo ""
echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${PREVIOUS_TAG}...${TAG}"

View File

@ -43,6 +43,10 @@ export function ContextMenu({
(e: globalThis.MouseEvent) => {
if (guard && !guard(e)) return
e.preventDefault()
// This stopPropagation is needed in case multiple nested items use a separate context menu each,
// which would cause the parent context menu to receive the event even if the child was clicked on.
// Eg. this happens in FileTree causing a bug where the parent folder is going into renaming mode when trying to rename a child.
e.stopPropagation()
setPosition({ x: e.clientX, y: e.clientY })
setOpen(true)
},

View File

@ -273,11 +273,11 @@ export const FileMachineProvider = ({
: newName + FILE_EXT
: DEFAULT_FILE_NAME
const oldPath = window.electron.path.join(
input.selectedDirectory.path,
input.parentDirectory.path,
oldName
)
const newPath = window.electron.path.join(
input.selectedDirectory.path,
input.parentDirectory.path,
name
)

View File

@ -69,37 +69,57 @@ function TreeEntryInput(props: {
function RenameForm({
fileOrDir,
parentDir,
onSubmit,
level = 0,
}: {
fileOrDir: FileEntry
parentDir: FileEntry | undefined
onSubmit: () => void
level?: number
}) {
const { send } = useFileContext()
const inputRef = useRef<HTMLInputElement>(null)
const fileContext = useFileContext()
function handleRenameSubmit(e: React.KeyboardEvent<HTMLElement>) {
if (e.key !== 'Enter') {
return
}
function handleRenameSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
send({
type: 'Rename file',
data: {
oldName: fileOrDir.name || '',
newName: inputRef.current?.value || fileOrDir.name || '',
isDir: fileOrDir.children !== null,
parentDirectory: parentDir ?? fileContext.context.project,
},
})
// To get out of the renaming state, without this the current file is still in renaming mode
onSubmit()
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Escape') {
e.stopPropagation()
onSubmit()
} else if (e.key === 'Enter') {
// This is needed to prevent events to bubble up and the form to be submitted.
// (Alternatively the form could be changed into a div.)
// Bug without this:
// - open a parent folder (close and open if it's already open)
// - right click -> rename one of its children
// - give new name and press enter
// -> new name is not applied, old name is reverted
e.preventDefault()
e.stopPropagation()
}
}
return (
<form onSubmit={handleRenameSubmit}>
<form onKeyUp={handleRenameSubmit}>
<label>
<span className="sr-only">Rename file</span>
<input
@ -156,7 +176,6 @@ const FileTreeItem = ({
parentDir,
project,
currentFile,
lastDirectoryClicked,
fileOrDir,
onNavigateToFile,
onClickDirectory,
@ -173,7 +192,6 @@ const FileTreeItem = ({
parentDir: FileEntry | undefined
project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file']
lastDirectoryClicked?: FileEntry
fileOrDir: FileEntry
onNavigateToFile?: () => void
onClickDirectory: (
@ -355,6 +373,7 @@ const FileTreeItem = ({
) : (
<RenameForm
fileOrDir={fileOrDir}
parentDir={parentDir}
onSubmit={removeCurrentItemFromRenaming}
level={level}
/>
@ -401,6 +420,7 @@ const FileTreeItem = ({
/>
<RenameForm
fileOrDir={fileOrDir}
parentDir={parentDir}
onSubmit={removeCurrentItemFromRenaming}
level={-1}
/>
@ -454,7 +474,6 @@ const FileTreeItem = ({
onCloneFileOrFolder={onCloneFileOrFolder}
onOpenInNewWindow={onOpenInNewWindow}
newTreeEntry={newTreeEntry}
lastDirectoryClicked={lastDirectoryClicked}
onClickDirectory={onClickDirectory}
onNavigateToFile={onNavigateToFile}
level={level + 1}
@ -690,10 +709,6 @@ export const FileTreeInner = ({
const { errors } = useKclContext()
const runtimeErrors = kclErrorsByFilename(errors)
const [lastDirectoryClicked, setLastDirectoryClicked] = useState<
FileEntry | undefined
>(undefined)
const [treeSelection, setTreeSelection] = useState<FileEntry | undefined>(
loaderData.file
)
@ -751,7 +766,6 @@ export const FileTreeInner = ({
if (!target) return
setTreeSelection(target)
setLastDirectoryClicked(target)
fileSend({
type: 'Set selected directory',
directory: target,
@ -787,7 +801,6 @@ export const FileTreeInner = ({
parentDir={fileContext.project}
project={fileContext.project}
currentFile={loaderData?.file}
lastDirectoryClicked={lastDirectoryClicked}
fileOrDir={fileOrDir}
onCreateFile={onCreateFile}
onCreateFolder={onCreateFolder}

View File

@ -12,11 +12,11 @@ import { NetworkHealthIndicator } from '@src/components/NetworkHealthIndicator'
import { NetworkMachineIndicator } from '@src/components/NetworkMachineIndicator'
import Tooltip from '@src/components/Tooltip'
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { PATHS } from '@src/lib/paths'
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
import { billingActor } from '@src/lib/singletons'
import { ActionButton } from '@src/components/ActionButton'
export function LowerRightControls({
children,
@ -52,15 +52,16 @@ export function LowerRightControls({
<BillingDialog billingActor={billingActor} />
</Popover.Panel>
</Popover>
<a
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()}
target="_blank"
rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
<ActionButton
Element="externalLink"
to={getReleaseUrl()}
className={
'!no-underline !border-none font-mono text-xs' +
linkOverrideClassName
}
>
v{APP_VERSION}
</a>
</ActionButton>
<Link
to={
location.pathname.includes(PATHS.FILE)

View File

@ -184,7 +184,7 @@ export const sidebarPanes: SidebarPane[] = [
// editorManager.scrollToFirstErrorDiagnosticIfExists()
},
className:
'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
'absolute m-0 p-0 bottom-4 left-4 min-w-3 h-3 flex items-center justify-center text-[9px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200',
title: 'Project files have runtime errors',
},
},

View File

@ -402,7 +402,7 @@ function ModelingPaneButton({
className={
showBadge.className
? showBadge.className
: 'absolute m-0 p-0 bottom-4 left-4 w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
: 'absolute m-0 p-0 bottom-4 left-4 min-w-3 h-3 flex items-center justify-center text-[10px] font-semibold text-white bg-primary hue-rotate-90 rounded-full border border-chalkboard-10 dark:border-chalkboard-80 z-50 hover:cursor-pointer hover:scale-[2] transition-transform duration-200'
}
onClick={showBadge.onClick}
title={

View File

@ -52,7 +52,7 @@ export function ProjectSearchBar({
ref={inputRef}
onChange={(event) => setQuery(event.target.value)}
className="w-full text-sm bg-transparent focus:outline-none selection:bg-primary/20 dark:selection:bg-primary/40 dark:focus:outline-none"
placeholder="Search projects (^.)"
placeholder="Search projects (Ctrl+.)"
autoCapitalize="off"
autoComplete="off"
autoCorrect="off"

View File

@ -5,7 +5,7 @@ import toast from 'react-hot-toast'
import { ActionButton } from '@src/components/ActionButton'
import { SafeRenderer } from '@src/lib/markdown'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { getReleaseUrl } from '@src/routes/utils'
import { getReleaseUrl, IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
export function ToastUpdate({
version,
@ -41,16 +41,20 @@ export function ToastUpdate({
v{version}
</span>
<p className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
href={getReleaseUrl(version)}
target="_blank"
rel="noreferrer"
>
here on GitHub.
</a>
A new update is available.
{!IS_NIGHTLY_OR_DEBUG && (
<span>
You can view the release notes{' '}
<a
onClick={openExternalBrowserIfDesktop(getReleaseUrl(version))}
href={getReleaseUrl(version)}
target="_blank"
rel="noreferrer"
>
here on GitHub.
</a>
</span>
)}
</p>
</div>
{releaseNotes && (

View File

@ -3,7 +3,7 @@ import { assign, fromPromise, setup } from 'xstate'
import type { FileEntry, Project } from '@src/lib/project'
type FileMachineContext = {
project: Project
project: Project // this is also the root directory
selectedDirectory: FileEntry
itemsBeingRenamed: (FileEntry | string)[]
}
@ -13,7 +13,12 @@ type FileMachineEvents =
| { type: 'Open file in new window'; data: { name: string } }
| {
type: 'Rename file'
data: { oldName: string; newName: string; isDir: boolean }
data: {
oldName: string
newName: string
isDir: boolean
parentDirectory: FileEntry
}
}
| {
type: 'Create file'
@ -145,7 +150,7 @@ export const fileMachine = setup({
oldName: string
newName: string
isDir: boolean
selectedDirectory: FileEntry
parentDirectory: FileEntry
}
}) => Promise.resolve({ message: '', newPath: '', oldPath: '' })
),
@ -324,14 +329,14 @@ export const fileMachine = setup({
oldName: '',
newName: '',
isDir: false,
selectedDirectory: {} as FileEntry,
parentDirectory: {} as FileEntry,
}
}
return {
oldName: event.data.oldName,
newName: event.data.newName,
isDir: event.data.isDir,
selectedDirectory: context.selectedDirectory,
parentDirectory: event.data.parentDirectory,
}
},

View File

@ -446,6 +446,9 @@ export function getAutoUpdater(): AppUpdater {
// Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'.
// It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976.
const { autoUpdater } = electronUpdater
// Allows us to rollback to a previous version if needed.
// See https://github.com/electron-userland/electron-builder/blob/7dbc6c77c340c869d1e7effa22135fc740003a0f/packages/electron-updater/src/AppUpdater.ts#L450-L451
autoUpdater.allowDowngrade = true
return autoUpdater
}

View File

@ -22,7 +22,9 @@ export const IS_NIGHTLY_OR_DEBUG =
IS_NIGHTLY || APP_VERSION === '0.0.0' || APP_VERSION === '11.22.33'
export function getReleaseUrl(version: string = APP_VERSION) {
return `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${version}`
if (IS_NIGHTLY_OR_DEBUG || version === 'main') {
return 'https://github.com/KittyCAD/modeling-app/commits/main'
}
return `https://github.com/KittyCAD/modeling-app/releases/tag/v${version}`
}