Compare commits

...

4 Commits

Author SHA1 Message Date
0c2f63b399 Fix yarn lint to use all files recursively 2025-01-12 15:47:33 -05:00
363ae10658 Upgrade typescript-eslint from 5.62.0 to 8.19.1 and remove eslint-config-react-app (#5006) 2025-01-11 09:59:09 -05:00
ac4a6c84cf Point-and-click Sweep (first PR) (#4989)
* Refactor 'Delete selection' as actor
Will fix #4662

* WIP logging

* WIP: working Solid3dGetExtrusionFaceInfo for loft

* Working wall deletion of loft

* Add offset plane deletion

* Add feature tree deletion of shell

* Clean up

* Revert "Clean up"

This reverts commit 214763cc2b.

* Clean up rust changes, taking the sketch with the most paths

* Working cap selection and deletion

* Clean up

* Add test for loft and offset plane deletion via selection

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* Set reenter: false as it was originally

* Passing test

* Add shell deletion via feature tree test

* Revert the migration to promise actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Use cmd.id as solid_id after latest engine merge

* Add feature tree deletion of offset plane and fix lint

* Add feature tree deletion of loft

* Clean up

* Better comment

* Lint fix

* Remove sketch sorting

* WIP: sweep point-and-click

* Working sweep

* Add test

* Make sweep a development command

* Fix tsc error

* Clean up for review

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-11 08:20:49 -05:00
c6fad2e2dc Add new lint to disallow use of confusing isNaN (#4999) 2025-01-11 05:28:12 +00:00
14 changed files with 1128 additions and 748 deletions

View File

@ -5,16 +5,24 @@
},
"plugins": [
"css-modules",
"jest",
"react",
"suggest-no-throw",
"@typescript-eslint"
],
"extends": [
"react-app",
"react-app/jest",
"plugin:css-modules/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"no-restricted-globals": [
"error",
{
"name": "isNaN",
"message": "Use Number.isNaN() instead."
}
],
"semi": [
"error",
"never"

View File

@ -14,6 +14,7 @@ export class ToolbarFixture {
extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
@ -40,6 +41,7 @@ export class ToolbarFixture {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')

View File

@ -934,6 +934,104 @@ loft001 = loft([sketch001, sketch002])
})
})
test(`Sweep point-and-click`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-500, %)
|> tangentialArcTo([-2000, 500], %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'profile',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '',
},
highlightedHeaderArg: 'profile',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Path: '',
Profile: '1 face',
},
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await clickOnSketch2()
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
await toolbar.openPane('code')
await editor.expectEditor.toContain(sweepDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [sweepDeclaration],
highlightedCode: '',
})
await toolbar.closePane('code')
})
await test.step('Delete sweep via feature tree selection', async () => {
await toolbar.openPane('feature-tree')
await page.waitForTimeout(500)
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
await page.waitForTimeout(500)
await toolbar.closePane('feature-tree')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
})
const shellPointAndClickCapCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },

View File

@ -91,8 +91,8 @@
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
"lint-fix": "eslint --fix src e2e packages/codemirror-lsp-client",
"lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client",
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
@ -171,8 +171,6 @@
"@types/uuid": "^9.0.8",
"@types/wicg-file-system-access": "^2023.10.5",
"@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17",
@ -182,9 +180,10 @@
"electron-builder": "24.13.3",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-suggest-no-throw": "^1.0.0",
"happy-dom": "^16.3.0",
"http-server": "^14.1.1",
@ -200,6 +199,7 @@
"tailwindcss": "^3.4.1",
"ts-node": "^10.0.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.19.1",
"vite": "^5.4.6",
"vite-plugin-package-version": "^1.1.0",
"vite-tsconfig-paths": "^4.3.2",

View File

@ -42,7 +42,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
// try to parse the content-length from the headers
const length = parseInt(match[1])
if (isNaN(length))
if (Number.isNaN(length))
return Promise.reject(new Error('invalid content length'))
// slice the headers since we now have the content length

View File

@ -157,39 +157,38 @@ export const ModelingMachineProvider = ({
'enable copilot': () => {
editorManager.setCopilotEnabled(true)
},
// tsc reports this typing as perfectly fine, but eslint is complaining.
// It's actually nonsensical, so I'm quieting.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
'sketch exit execute': async ({
context: { store },
}): Promise<void> => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
'sketch exit execute': ({ context: { store } }) => {
// TODO: Remove this async callback. For some reason eslint wouldn't
// let me disable @typescript-eslint/no-misused-promises for the line.
;(async () => {
// When cancelling the sketch mode we should disable sketch mode within the engine.
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'sketch_mode_disable' },
})
.catch(reportRejection)
sceneInfra.camControls.syncDirection = 'clientToEngine'
if (cameraProjection.current === 'perspective') {
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
}
sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause()
return kclManager
.executeCode()
.then(() => {
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => {
console.warn('Video playing was prevented', e)
})
})
.catch(reportRejection)
})().catch(reportRejection)
},
'Set mouse state': assign(({ context, event }) => {
if (event.type !== 'Set mouse state') return {}

View File

@ -374,6 +374,37 @@ export function loftSketches(
}
}
export function addSweep(
node: Node<Program>,
profileDeclarator: VariableDeclarator,
pathDeclarator: VariableDeclarator
): {
modifiedAst: Node<Program>
pathToNode: PathToNode
} {
const modifiedAst = structuredClone(node)
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const sweep = createCallExpressionStdLib('sweep', [
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
createIdentifier(profileDeclarator.id.name),
])
const declaration = createVariableDeclaration(name, sweep)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst,
pathToNode,
}
}
export function revolveSketch(
node: Node<Program>,
pathToNode: PathToNode,

View File

@ -77,7 +77,7 @@ interface SegmentArtifactRich extends BaseArtifact {
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
@ -85,7 +85,7 @@ interface SweepArtifact extends BaseArtifact {
}
interface SweepArtifactRich extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge>
@ -377,7 +377,11 @@ export function getArtifactsToUpdate({
})
}
return returnArr
} else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
} else if (
cmd.type === 'extrude' ||
cmd.type === 'revolve' ||
cmd.type === 'sweep'
) {
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
returnArr.push({
id,

View File

@ -37,6 +37,10 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue
}
Sweep: {
path: Selections
profile: Selections
}
Loft: {
selection: Selections
}
@ -292,6 +296,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
Sweep: {
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep',
status: 'development',
needsReview: true,
args: {
profile: {
inputType: 'selection',
selectionTypes: ['solid2D'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.',
},
path: {
inputType: 'selection',
selectionTypes: ['segment', 'path'],
required: true,
skip: true,
multiple: false,
// TODO: add dry-run validation
},
},
},
Loft: {
description: 'Create a 3D body by blending between two or more sketches',
icon: 'loft',

View File

@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch',
EXTRUDE: 'extrude',
LOFT: 'loft',
SWEEP: 'sweep',
SHELL: 'shell',
SEGMENT: 'seg',
REVOLVE: 'revolve',

View File

@ -119,17 +119,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'sweep',
onClick: () => console.error('Sweep not yet implemented'),
onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Sweep', groupId: 'modeling' },
}),
icon: 'sweep',
status: 'unavailable',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Sweep',
hotkey: 'W',
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/498',
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/sweep',
},
],
},

View File

@ -345,7 +345,7 @@ export function onDragNumberCalculation(text: string, e: MouseEvent) {
)
const newVal = roundOff(addition, precision)
if (isNaN(newVal)) {
if (Number.isNaN(newVal)) {
return
}

View File

@ -45,6 +45,7 @@ import {
import { revolveSketch } from 'lang/modifyAst/addRevolve'
import {
addOffsetPlane,
addSweep,
deleteFromSelection,
extrudeSketch,
loftSketches,
@ -266,6 +267,7 @@ export type ModelingMachineEvent =
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Sweep'; data?: ModelingCommandSchema['Sweep'] }
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
@ -1544,6 +1546,66 @@ export const modelingMachine = setup({
}
}
),
sweepAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Sweep'] | undefined
}) => {
if (!input) return new Error('No input provided')
// Extract inputs
const ast = kclManager.ast
const { profile, path } = input
// Find the profile declaration
const profileNodePath = getNodePathFromSourceRange(
ast,
profile.graphSelections[0].codeRef.range
)
const profileNode = getNodeFromPath<VariableDeclarator>(
ast,
profileNodePath,
'VariableDeclarator'
)
if (err(profileNode)) {
return new Error("Couldn't parse profile selection")
}
const profileDeclarator = profileNode.node
// Find the path declaration
const pathNodePath = getNodePathFromSourceRange(
ast,
path.graphSelections[0].codeRef.range
)
const pathNode = getNodeFromPath<VariableDeclarator>(
ast,
pathNodePath,
'VariableDeclarator'
)
if (err(pathNode)) {
return new Error("Couldn't parse path selection")
}
const pathDeclarator = pathNode.node
// Perform the sweep
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
const updateAstResult = await kclManager.updateAst(
sweepRes.modifiedAst,
true,
{
focusPath: [sweepRes.pathToNode],
}
)
await codeManager.updateEditorWithAstAndWriteToFile(
updateAstResult.newAst
)
if (updateAstResult?.selections) {
editorManager.selectRange(updateAstResult?.selections)
}
}
),
loftAstMod: fromPromise(
async ({
input,
@ -1739,6 +1801,11 @@ export const modelingMachine = setup({
reenter: false,
},
Sweep: {
target: 'Applying sweep',
reenter: true,
},
Loft: {
target: 'Applying loft',
reenter: true,
@ -2531,6 +2598,19 @@ export const modelingMachine = setup({
},
},
'Applying sweep': {
invoke: {
src: 'sweepAstMod',
id: 'sweepAstMod',
input: ({ event }) => {
if (event.type !== 'Sweep') return undefined
return event.data
},
onDone: ['idle'],
onError: ['idle'],
},
},
'Applying loft': {
invoke: {
src: 'loftAstMod',

1522
yarn.lock

File diff suppressed because it is too large Load Diff