Compare commits

...

6 Commits

32 changed files with 318 additions and 182 deletions

View File

@ -5,6 +5,7 @@ on:
push:
branches:
- main
- cut-release-v0.25.1-updater-test-build-1
release:
types: [published]
schedule:
@ -13,8 +14,8 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04)
env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
CUT_RELEASE_PR: true
BUILD_RELEASE: true
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@ -44,7 +45,7 @@ jobs:
# TODO: see if we can fetch from main instead if no diff at src/wasm-lib
- name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}"
run: "yarn build:wasm"
- name: Set nightly version
if: github.event_name == 'schedule'
@ -156,15 +157,15 @@ jobs:
runs-on: ubuntu-22.04
permissions:
contents: write
if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
# if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }}
needs: [prepare-files, build-apps]
env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }}
PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }}
NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app/test/cut-release-v0.25.1-updater-test' }}
WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app/test/cut-release-v0.25.1-updater-test' }}
URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps:
- uses: actions/checkout@v4
@ -245,6 +246,15 @@ jobs:
parent: false
destination: ${{ env.BUCKET_DIR }}
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
- name: Upload release files to public bucket (test/electron-builder workaround)
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'Zoo*'
parent: false
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
@ -253,6 +263,15 @@ jobs:
parent: false
destination: ${{ env.BUCKET_DIR }}
# TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817
- name: Upload update endpoint to public bucket (test/electron-builder workaround)
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:
path: out
glob: 'latest*'
parent: false
destination: '${{ env.BUCKET_DIR }}/test/electron-builder'
- name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0
with:

View File

@ -28,6 +28,7 @@ jobs:
dir: ['src/wasm-lib']
steps:
- uses: actions/checkout@v4
- uses: taiki-e/install-action@just
- name: Install latest rust
uses: actions-rs/toolchain@v1
with:
@ -41,7 +42,7 @@ jobs:
- name: Run clippy
run: |
cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings
just lint
# If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -112,7 +112,8 @@ test.describe('when using the file tree to', () => {
})
const {
panesOpen,
openKclCodePanel,
openFilePanel,
createAndSelectProject,
pasteCodeInEditor,
createNewFileAndSelect,
@ -124,9 +125,9 @@ test.describe('when using the file tree to', () => {
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',

View File

@ -548,13 +548,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter')
await page
const newFile = page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name })
.click()
await expect(newFile).toBeVisible()
await newFile.click()
})
},
@ -585,6 +588,15 @@ export async function getUtils(page: Page, test_?: typeof test) {
})
},
/**
* @deprecated Sorry I don't have time to fix this right now, but runs like
* the one linked below show me that setting the open panes in this manner is not reliable.
* You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup,
* or you can imperatively open the panes with functions like {openKclCodePanel}
* (or we can make a general openPane function that takes a paneId).,
* but having a separate initScript does not seem to work reliably.
* @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553
*/
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(

View File

@ -288,7 +288,7 @@ test.describe('Testing settings', () => {
})
await test.step('Refresh the application and see project setting applied', async () => {
await page.reload()
await page.reload({ waitUntil: 'domcontentloaded' })
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click()
@ -364,47 +364,48 @@ test.describe('Testing settings', () => {
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'project-000')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('cube.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('cylinder.kcl'),
join(bracketDir, '2.kcl')
)
},
})
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
const kclCylinder = await fsp.readFile(
executorInputPath('cylinder.kcl'),
'utf8'
)
const {
panesOpen,
createAndSelectProject,
pasteCodeInEditor,
clickPane,
createNewFileAndSelect,
openKclCodePanel,
openFilePanel,
waitForPageLoad,
selectFile,
editorTextMatches,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await panesOpen([])
await test.step('Precondition: No projects exist', async () => {
await test.step('Precondition: Open to second project file', async () => {
await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link')
await expect(projectLinksPre).toHaveCount(0)
await page.getByText('project-000').click()
await waitForPageLoad()
await openKclCodePanel()
await openFilePanel()
await editorTextMatches(kclCube)
await selectFile('2.kcl')
await editorTextMatches(kclCylinder)
})
await createAndSelectProject('project-000')
await clickPane('code')
const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCube)
await clickPane('files')
await createNewFileAndSelect('2.kcl')
const kclCylinder = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
'utf-8'
)
await pasteCodeInEditor(kclCylinder)
const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings',
})
@ -412,6 +413,9 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => {
await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click()
})

View File

@ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
// Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model.
@ -690,40 +690,53 @@ test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () =>
fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl'))
fs.existsSync(join(dir, projectName, textToCadFileName))
const { createAndSelectProject, panesOpen } = await getUtils(page, test)
const {
createAndSelectProject,
openFilePanel,
openKclCodePanel,
waitForPageLoad,
} = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 })
await panesOpen(['code', 'files'])
// Locators
const projectMenuButton = page.getByRole('button', { name: projectName })
const textToCadFileButton = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: textToCadFileName }),
})
const textToCadComment = page.getByText(
`// Generated by Text-to-CAD: ${prompt}`
)
// Create and navigate to the project
await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
await waitForPageLoad()
await openFilePanel()
await openKclCodePanel()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBar(page, 'lego 2x4')
await sendPromptFromCommandBar(page, prompt)
// File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await expect(file).toBeVisible({ timeout: 20_000 })
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy()
})
await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' })
await file.click()
const kclComment = page.getByText('Lego 2x4 Brick')
await expect(projectMenuButton).toContainText('main.kcl')
await textToCadFileButton.click()
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
await expect(kclComment).toBeVisible({ timeout: 20_000 })
await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
await expect(projectMenuButton).toContainText(textToCadFileName)
})
await test.step(`Test file deletion on rejection`, async () => {
@ -737,6 +750,8 @@ test(
)
await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()

View File

@ -79,5 +79,5 @@ linux:
publish:
- provider: generic
url: https://dl.zoo.dev/releases/modeling-app/test/electron-builder
url: https://dl.zoo.dev/releases/modeling-app/test/cut-release-v0.25.1-updater-test
channel: latest

View File

@ -1,6 +1,6 @@
{
"name": "zoo-modeling-app",
"version": "0.25.0",
"version": "0.25.1",
"private": true,
"productName": "Zoo Modeling App",
"author": {

View File

@ -8,7 +8,7 @@ import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange, parse, recast } from 'lang/wasm'
import { PathToNode, SourceRange } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -23,8 +23,7 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable])
useEffect(() => {
const parsed = parse(recast(ast))
if (trap(parsed)) return
const parsed = ast
const meta = isNodeSafeToReplace(
parsed,

View File

@ -56,11 +56,6 @@ body.dark {
.dark .body-bg {
@apply bg-chalkboard-100;
}
body {
scrollbar-color: var(--color-chalkboard-70) var(--color-chalkboard-90);
@apply text-chalkboard-10;
}
}
select {
@ -300,32 +295,11 @@ code {
}
@layer utilities {
/* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */
@keyframes circle-in-hesitate {
0% {
clip-path: circle(
var(--circle-size-start, 0%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
40% {
clip-path: circle(
var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%)
);
}
100% {
clip-path: circle(
var(--circle-size-end, 125%) at var(--circle-x, 50%)
var(--circle-y, 50%)
);
}
}
.in-circle-hesitate {
animation: var(--circle-duration, 2.5s)
var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate
both;
}
/*
This is where your own custom Tailwind utility classes can go,
which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX.
*/
}
#code-mirror-override .cm-scroller,

View File

@ -5,7 +5,7 @@ import {
kclManager,
sceneEntitiesManager,
} from 'lib/singletons'
import { CallExpression, SourceRange, Expr, parse, recast } from 'lang/wasm'
import { CallExpression, SourceRange, Expr, parse } from 'lang/wasm'
import { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state'
@ -300,8 +300,7 @@ export function processCodeMirrorRanges({
}
function updateSceneObjectColors(codeBasedSelections: Selection[]) {
const updated = parse(recast(kclManager.ast))
if (err(updated)) return
const updated = kclManager.ast
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if (

View File

@ -70,17 +70,11 @@ const SignIn = () => {
>
<div
style={
{
height: 'calc(100vh - 16px)',
'--circle-x': '14%',
'--circle-y': '12%',
'--circle-size-mid': '15%',
'--circle-size-end': '200%',
'--circle-timing': 'cubic-bezier(0.25, 1, 0.4, 0.9)',
...(isDesktop() ? { '-webkit-app-region': 'no-drag' } : {}),
} as CSSProperties
isDesktop()
? ({ '-webkit-app-region': 'no-drag' } as CSSProperties)
: {}
}
className="in-circle-hesitate body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto"
>
<div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5">
<div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8">
@ -204,7 +198,7 @@ const SignIn = () => {
<div className="flex gap-4 flex-wrap items-center">
<ActionButton
Element="externalLink"
to="https://zoo.dev/docs/kcl-samples/ball-bearing"
to="https://zoo.dev/docs/kcl-samples/a-parametric-bearing-pillow-block"
iconStart={{ icon: 'settings' }}
className="border-chalkboard-30 dark:border-chalkboard-80"
>

View File

@ -620,9 +620,9 @@ dependencies = [
[[package]]
name = "dashmap"
version = "6.0.1"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [
"cfg-if",
"crossbeam-utils",
@ -1357,7 +1357,7 @@ dependencies = [
"clap",
"convert_case",
"criterion",
"dashmap 6.0.1",
"dashmap 6.1.0",
"databake",
"derive-docs",
"expectorate",
@ -1399,7 +1399,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winnow 0.5.40",
"winnow",
"zip",
]
@ -3117,7 +3117,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.18",
"winnow",
]
[[package]]
@ -3800,15 +3800,6 @@ version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.18"

View File

@ -2,3 +2,6 @@
new-test name:
echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs
TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})'
lint:
cargo clippy --all --tests --benches -- -D warnings

View File

@ -18,7 +18,7 @@ base64 = "0.22.1"
chrono = "0.4.38"
clap = { version = "4.5.17", default-features = false, optional = true, features = ["std", "derive"] }
convert_case = "0.6.0"
dashmap = "6.0.1"
dashmap = "6.1.0"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.26", path = "../derive-docs" }
form_urlencoded = "1.2.1"
@ -47,7 +47,7 @@ url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40"
winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::test_server;
use kcl_lib::{settings::types::UnitLength::Mm, test_server};
use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) {
@ -13,26 +13,42 @@ pub fn bench_execute(c: &mut Criterion) {
// Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise.
group.sample_size(10);
group.bench_with_input(BenchmarkId::new("execute_", name), &code, |b, &s| {
group.bench_with_input(BenchmarkId::new("execute", name), &code, |b, &s| {
let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime
b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(
s,
kcl_lib::settings::types::UnitLength::Mm,
))
.unwrap();
rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap();
});
});
group.finish();
}
}
criterion_group!(benches, bench_execute);
pub fn bench_lego(c: &mut Criterion) {
let mut group = c.benchmark_group("executor_lego_pattern");
// Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise.
group.sample_size(10);
// Create lego bricks with N x 10 bumps, where N is each element of `sizes`.
let sizes = vec![1, 2, 4];
for size in sizes {
group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| {
let rt = Runtime::new().unwrap();
let code = LEGO_PROGRAM.replace("{{N}}", &size.to_string());
// Spawn a future onto the runtime
b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(&code, Mm)).unwrap();
});
});
}
group.finish();
}
criterion_group!(benches, bench_lego, bench_execute);
criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");
const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl");
const LEGO_PROGRAM: &str = include_str!("../../tests/executor/inputs/slow_lego.kcl.tmpl");

View File

@ -927,7 +927,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
match body_items_within_function.parse_next(i) {
Err(ErrMode::Backtrack(_)) => {
i.reset(start);
i.reset(&start);
break;
}
Err(e) => return Err(e),
@ -937,7 +937,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
}
}
(Err(ErrMode::Backtrack(_)), _) => {
i.reset(start);
i.reset(&start);
break;
}
(Err(e), _) => return Err(e),
@ -1276,7 +1276,7 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
/// Consume tokens that make up a binary expression, but don't actually return them.
/// Why not?
/// Because this is designed to be used with .recognize() within the `binary_expression` parser.
/// Because this is designed to be used with .take() within the `binary_expression` parser.
fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> {
let first = operand.parse_next(i).map(BinaryExpressionToken::from)?;
let remaining: Vec<_> = repeat(
@ -1308,7 +1308,7 @@ fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> {
}
fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> {
let span_with_brackets = bracketed_section.recognize().parse_next(i)?;
let span_with_brackets = bracketed_section.take().parse_next(i)?;
let n = span_with_brackets.len();
let mut span_no_brackets = &span_with_brackets[1..n - 1];
let expr = binary_expression.parse_next(&mut span_no_brackets)?;

View File

@ -1,5 +1,6 @@
use winnow::{
error::{ErrorKind, ParseError, StrContext},
stream::Stream,
Located,
};
@ -102,14 +103,17 @@ impl<C> std::default::Default for ContextError<C> {
}
}
impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
impl<I, C> winnow::error::ParserError<I> for ContextError<C>
where
I: Stream,
{
#[inline]
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
Self::default()
}
#[inline]
fn append(self, _input: &I, _kind: ErrorKind) -> Self {
fn append(self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, _kind: ErrorKind) -> Self {
self
}
@ -119,9 +123,12 @@ impl<I, C> winnow::error::ParserError<I> for ContextError<C> {
}
}
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> {
impl<C, I> winnow::error::AddContext<I, C> for ContextError<C>
where
I: Stream,
{
#[inline]
fn add_context(mut self, _input: &I, ctx: C) -> Self {
fn add_context(mut self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, ctx: C) -> Self {
self.context.push(ctx);
self
}

View File

@ -1,8 +1,10 @@
//! Functions related to extruding.
use std::collections::HashMap;
use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ExtrusionFaceCapType;
use kittycad::types::{ExtrusionFaceCapType, ExtrusionFaceInfo};
use schemars::JsonSchema;
use uuid::Uuid;
@ -99,7 +101,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
)
.await?;
args.send_modeling_cmd(
args.batch_modeling_cmd(
id,
kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id,
@ -112,7 +114,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
// Disable the sketch mode.
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
.await?;
extrude_groups.push(do_post_extrude(sketch_group.clone(), length, id, args.clone()).await?);
extrude_groups.push(do_post_extrude(sketch_group.clone(), length, args.clone()).await?);
}
Ok(extrude_groups.into())
@ -121,7 +123,6 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
pub(crate) async fn do_post_extrude(
sketch_group: SketchGroup,
length: f64,
id: Uuid,
args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
// Bring the object to the front of the scene.
@ -165,7 +166,7 @@ pub(crate) async fn do_post_extrude(
let solid3d_info = args
.send_modeling_cmd(
id,
uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo {
edge_id,
object_id: sketch_group.id,
@ -218,26 +219,11 @@ pub(crate) async fn do_post_extrude(
.await?;
}
// Create a hashmap for quick id lookup
let mut face_id_map = std::collections::HashMap::new();
// creating fake ids for start and end caps is to make extrudes mock-execute safe
let (mut start_cap_id, mut end_cap_id) = if args.ctx.is_mock {
(Some(Uuid::new_v4()), Some(Uuid::new_v4()))
} else {
(None, None)
};
for face_info in face_infos {
match face_info.cap {
ExtrusionFaceCapType::Bottom => start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => end_cap_id = face_info.face_id,
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
face_id_map.insert(curve_id, face_info.face_id);
}
}
}
}
let Faces {
sides: face_id_map,
start_cap_id,
end_cap_id,
} = analyze_faces(&args, face_infos);
// Iterate over the sketch_group.value array and add face_id to GeoMeta
let new_value = sketch_group
.value
@ -301,3 +287,37 @@ pub(crate) async fn do_post_extrude(
edge_cuts: vec![],
}))
}
#[derive(Default)]
struct Faces {
/// Maps curve ID to face ID for each side.
sides: HashMap<Uuid, Option<Uuid>>,
/// Top face ID.
end_cap_id: Option<Uuid>,
/// Bottom face ID.
start_cap_id: Option<Uuid>,
}
fn analyze_faces(args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
let mut faces = Faces {
sides: HashMap::with_capacity(face_infos.len()),
..Default::default()
};
if args.ctx.is_mock {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(Uuid::new_v4());
faces.end_cap_id = Some(Uuid::new_v4());
}
for face_info in face_infos {
match face_info.cap {
ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id,
ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id,
ExtrusionFaceCapType::None => {
if let Some(curve_id) = face_info.curve_id {
faces.sides.insert(curve_id, face_info.face_id);
}
}
}
}
faces
}

View File

@ -170,5 +170,5 @@ async fn inner_loft(
.await?;
// Using the first sketch as the base curve, idk we might want to change this later.
do_post_extrude(sketch_groups[0].clone(), 0.0, id, args).await
do_post_extrude(sketch_groups[0].clone(), 0.0, args).await
}

View File

@ -299,7 +299,7 @@ async fn inner_revolve(
}
}
do_post_extrude(sketch_group, 0.0, id, args).await
do_post_extrude(sketch_group, 0.0, args).await
}
#[cfg(test)]

View File

@ -50,13 +50,13 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
}
fn block_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = ("/*", take_until(0.., "*/"), "*/").recognize();
let inner = ("/*", take_until(0.., "*/"), "*/").take();
let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::BlockComment, value.to_string()))
}
fn line_comment(i: &mut Located<&str>) -> PResult<Token> {
let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).recognize();
let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).take();
let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::LineComment, value.to_string()))
}
@ -68,7 +68,7 @@ fn number(i: &mut Located<&str>) -> PResult<Token> {
// No digits before the decimal point.
('.', digit1).map(|_| ()),
));
let (value, range) = number_parser.recognize().with_span().parse_next(i)?;
let (value, range) = number_parser.take().with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::Number, value.to_string()))
}
@ -84,7 +84,7 @@ fn inner_word(i: &mut Located<&str>) -> PResult<()> {
}
fn word(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = inner_word.recognize().with_span().parse_next(i)?;
let (value, range) = inner_word.take().with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::Word, value.to_string()))
}
@ -162,9 +162,9 @@ fn inner_single_quote(i: &mut Located<&str>) -> PResult<()> {
}
fn string(i: &mut Located<&str>) -> PResult<Token> {
let single_quoted_string = ('\'', inner_single_quote.recognize(), '\'');
let double_quoted_string = ('"', inner_double_quote.recognize(), '"');
let either_quoted_string = alt((single_quoted_string.recognize(), double_quoted_string.recognize()));
let single_quoted_string = ('\'', inner_single_quote.take(), '\'');
let double_quoted_string = ('"', inner_double_quote.take(), '"');
let either_quoted_string = alt((single_quoted_string.take(), double_quoted_string.take()));
let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::String, value.to_string()))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -0,0 +1,81 @@
// 2x8 Lego Brick
// A standard Lego brick with 2 bumps wide and 8 bumps long.
// Define constants
const lbumps = 10 // number of bumps long
const wbumps = {{N}} // number of bumps wide
const pitch = 8.0
const clearance = 0.1
const bumpDiam = 4.8
const bumpHeight = 1.8
const height = 9.6
const t = (pitch - (2 * clearance) - bumpDiam) / 2.0
const totalLength = lbumps * pitch - (2.0 * clearance)
const totalWidth = wbumps * pitch - (2.0 * clearance)
// Create the plane for the pegs. This is a hack so that the pegs can be patterned along the face of the lego base.
const pegFace = {
plane: {
origin: { x: 0, y: 0, z: height },
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }
}
}
// Create the plane for the tubes underneath the lego. This is a hack so that the tubes can be patterned underneath the lego.
const tubeFace = {
plane: {
origin: { x: 0, y: 0, z: height - t },
xAxis: { x: 1, y: 0, z: 0 },
yAxis: { x: 0, y: 1, z: 0 },
zAxis: { x: 0, y: 0, z: 1 }
}
}
// Make the base
const s = startSketchOn('XY')
|> startProfileAt([-totalWidth / 2, -totalLength / 2], %)
|> line([totalWidth, 0], %)
|> line([0, totalLength], %)
|> line([-totalWidth, 0], %)
|> close(%)
|> extrude(height, %)
// Sketch and extrude a rectangular shape to create the shell underneath the lego. This is a hack until we have a shell function.
const shellExtrude = startSketchOn(s, "start")
|> startProfileAt([
-(totalWidth / 2 - t),
-(totalLength / 2 - t)
], %)
|> line([totalWidth - (2 * t), 0], %)
|> line([0, totalLength - (2 * t)], %)
|> line([-(totalWidth - (2 * t)), 0], %)
|> close(%)
|> extrude(-(height - t), %)
fn tr = (i) => {
let j = i + 1
let x = (j/wbumps) * pitch
let y = (j % wbumps) * pitch
return {
translate: [x, y, 0],
}
}
// Create the pegs on the top of the base
const totalBumps = (wbumps * lbumps)-1
const peg = startSketchOn(s, 'end')
|> circle([
-(pitch*(wbumps-1)/2),
-(pitch*(lbumps-1)/2)
], bumpDiam / 2, %)
|> patternLinear2d({
axis: [1, 0],
repetitions: wbumps-1,
distance: pitch
}, %)
|> patternLinear2d({
axis: [0, 1],
repetitions: lbumps-1,
distance: pitch
}, %)
|> extrude(bumpHeight, %)
// |> patternTransform(int(totalBumps-1), tr, %)