Compare commits

...

7 Commits

Author SHA1 Message Date
9b29b08f62 Bugfix: Extrude and GetExtrusionFaceInfo were using the same cmd ID 2024-09-06 15:49:56 -05:00
46c613288c Batch extrudes
Adds a new KCL test which builds a NxN lego. This change improves it by
25-36% depending on how many bumps there are.
2024-09-06 15:49:47 -05:00
541dcd9a41 Merge branch 'main' into franknoirot/get-playwright-green 2024-09-06 15:32:40 -04:00
b5bf85d420 Fix mistake in selector for text-to-cad test 2024-09-06 15:02:30 -04:00
4eee73f471 Seeing if paneOpen is part of the run order-dependent story 2024-09-06 14:46:21 -04:00
7462eee89c Make "Closing settings modal should go back..." test not run order-dependent 2024-09-06 14:33:51 -04:00
17945cac6d Fix "Text-to-CAD functionality" electron test 2024-09-06 14:26:00 -04:00
9 changed files with 158 additions and 39 deletions

View File

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

View File

@ -585,6 +585,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[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(

View File

@ -368,10 +368,10 @@ test.describe('Testing settings', () => {
}) })
const { const {
panesOpen, openKclCodePanel,
openFilePanel,
createAndSelectProject, createAndSelectProject,
pasteCodeInEditor, pasteCodeInEditor,
clickPane,
createNewFileAndSelect, createNewFileAndSelect,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(page, test)
@ -379,8 +379,6 @@ test.describe('Testing settings', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen([])
await test.step('Precondition: No projects exist', async () => { await test.step('Precondition: No projects exist', async () => {
await expect(page.getByTestId('home-section')).toBeVisible() await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link') const projectLinksPre = page.getByTestId('project-link')
@ -389,14 +387,14 @@ test.describe('Testing settings', () => {
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
await clickPane('code') await openKclCodePanel()
const kclCube = await fsp.readFile( const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl', 'src/wasm-lib/tests/executor/inputs/cube.kcl',
'utf-8' 'utf-8'
) )
await pasteCodeInEditor(kclCube) await pasteCodeInEditor(kclCube)
await clickPane('files') await openFilePanel()
await createNewFileAndSelect('2.kcl') await createNewFileAndSelect('2.kcl')
const kclCylinder = await fsp.readFile( const kclCylinder = await fsp.readFile(

View File

@ -690,40 +690,53 @@ test(
'Text-to-CAD functionality', 'Text-to-CAD functionality',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo }) const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () => 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 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 // Create and navigate to the project
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command // Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect( await waitForPageLoad()
page.getByRole('button', { name: 'Start Sketch' }) await openFilePanel()
).toBeEnabled({ await openKclCodePanel()
timeout: 20_000,
})
await test.step(`Test file creation`, async () => { 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 // File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
await expect(file).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy() expect(fileExists()).toBeTruthy()
}) })
await test.step(`Test file navigation`, async () => { await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) await expect(projectMenuButton).toContainText('main.kcl')
await file.click() await textToCadFileButton.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane // 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 () => { await test.step(`Test file deletion on rejection`, async () => {
@ -737,6 +750,8 @@ test(
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy() expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 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; use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) { 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 // Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise. // precision and counteract the resulting noise.
group.sample_size(10); 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(); let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime // Spawn a future onto the runtime
b.iter(|| { b.iter(|| {
rt.block_on(test_server::execute_and_snapshot( rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap();
s,
kcl_lib::settings::types::UnitLength::Mm,
))
.unwrap();
}); });
}); });
group.finish(); 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); criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl"); const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.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_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 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

@ -99,7 +99,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
) )
.await?; .await?;
args.send_modeling_cmd( args.batch_modeling_cmd(
id, id,
kittycad::types::ModelingCmd::Extrude { kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id, target: sketch_group.id,
@ -112,7 +112,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
// Disable the sketch mode. // Disable the sketch mode.
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {}) args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
.await?; .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()) Ok(extrude_groups.into())
@ -121,7 +121,6 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
pub(crate) async fn do_post_extrude( pub(crate) async fn do_post_extrude(
sketch_group: SketchGroup, sketch_group: SketchGroup,
length: f64, length: f64,
id: Uuid,
args: Args, args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> { ) -> Result<Box<ExtrudeGroup>, KclError> {
// Bring the object to the front of the scene. // Bring the object to the front of the scene.
@ -165,7 +164,7 @@ pub(crate) async fn do_post_extrude(
let solid3d_info = args let solid3d_info = args
.send_modeling_cmd( .send_modeling_cmd(
id, uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo {
edge_id, edge_id,
object_id: sketch_group.id, object_id: sketch_group.id,

View File

@ -170,5 +170,5 @@ async fn inner_loft(
.await?; .await?;
// Using the first sketch as the base curve, idk we might want to change this later. // 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)] #[cfg(test)]

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, %)