* draft: fillet ast mod + test

* Kurt's rejig

* playwright

* update button enable logic

* remove fillet button in production build

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* trigger CI

* fix typo

* give a way to turn on fillets

---------

Co-authored-by: max-mrgrsk <margorskyi@gmail.com>
Co-authored-by: max-mrgrsk <156543465+max-mrgrsk@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Kurt Hutten
2024-07-15 19:20:32 +10:00
committed by GitHub
parent 1852e6167b
commit a1df3d0ffc
28 changed files with 1066 additions and 26 deletions

View File

@ -3099,6 +3099,49 @@ const sketch002 = startSketchOn(extrude001, $seg01)
).not.toBeDisabled() ).not.toBeDisabled()
}) })
test('Fillet button states test', async ({ page }) => {
const u = await getUtils(page)
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`
)
})
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
const selectClose = () => page.getByText(`close(%)`).click()
const clickEmpty = () => page.mouse.click(950, 100)
// expect fillet button without any bodies in the scene
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
// test fillet button with the body in the scene
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
const extrude001 = extrude(10, sketch001)`
await u.codeLocator.fill(codeToAdd)
await selectSegment()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
await selectClose()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeDisabled()
await clickEmpty()
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
})
const removeAfterFirstParenthesis = (inputString: string) => { const removeAfterFirstParenthesis = (inputString: string) => {
const index = inputString.indexOf('(') const index = inputString.indexOf('(')
if (index !== -1) { if (index !== -1) {
@ -3500,6 +3543,44 @@ test.describe('Command bar tests', () => {
`const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)` `const extrude001 = extrude(${KCL_DEFAULT_LENGTH}, sketch001)`
) )
}) })
test('Fillet from command bar', async ({ page }) => {
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`const sketch001 = startSketchOn('XY')
|> startProfileAt([-5, -5], %)
|> line([0, 10], %)
|> line([10, 0], %)
|> line([0, -10], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(-10, sketch001)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
await selectSegment()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Fillet' }).click()
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await page.keyboard.press('Enter')
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toContainText(
`fillet({ radius: ${KCL_DEFAULT_LENGTH}, tags: [seg01] }, %)`
)
})
test('Command bar can change a setting, and switch back and forth between arguments', async ({ test('Command bar can change a setting, and switch back and forth between arguments', async ({
page, page,
}) => { }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -16,6 +16,7 @@ import {
canRectangleTool, canRectangleTool,
isEditingExistingSketch, isEditingExistingSketch,
} from 'machines/modelingMachine' } from 'machines/modelingMachine'
import { DEV } from 'env'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -118,6 +119,16 @@ export function Toolbar({
}), }),
{ enabled: !disableAllButtons, scopes: ['modeling'] } { enabled: !disableAllButtons, scopes: ['modeling'] }
) )
const disableFillet = !state.can('Fillet') || disableAllButtons
useHotkeys(
'f',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
}),
{ enabled: !disableFillet, scopes: ['modeling'] }
)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) { function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current const span = toolbarButtonsRef.current
@ -404,6 +415,36 @@ export function Toolbar({
</ActionButton> </ActionButton>
</li> </li>
)} )}
{state.matches('idle') && (DEV || (window as any)._enableFillet) && (
<li className="contents">
<ActionButton
className={buttonClassName}
Element="button"
onClick={() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Fillet', groupId: 'modeling' },
})
}
disabled={disableFillet}
title={disableFillet ? 'fillet' : "edge can't be filleted"}
iconStart={{
icon: 'fillet', // todo: add fillet icon
iconClassName,
bgClassName,
}}
>
Fillet
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: F
</Tooltip>
</ActionButton>
</li>
)}
</ul> </ul>
</menu> </menu>
) )

View File

@ -11,6 +11,25 @@ import { modelingMachine } from 'machines/modelingMachine'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { StateFrom } from 'xstate' import { StateFrom } from 'xstate'
const semanticEntityNames = {
face: ['extrude-wall', 'start-cap', 'end-cap'],
edge: ['edge', 'line', 'arc'],
point: ['point', 'line-end', 'line-mid'],
}
function getSemanticSelectionType(selectionType: string[]) {
const semanticSelectionType = new Set()
selectionType.forEach((type) => {
Object.entries(semanticEntityNames).forEach(([entity, entityTypes]) => {
if (entityTypes.includes(type)) {
semanticSelectionType.add(entity)
}
})
})
return Array.from(semanticSelectionType)
}
const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) => const selectionSelector = (snapshot: StateFrom<typeof modelingMachine>) =>
snapshot.context.selectionRanges snapshot.context.selectionRanges
@ -85,7 +104,9 @@ function CommandBarSelectionInput({
> >
{canSubmitSelection {canSubmitSelection
? getSelectionTypeDisplayText(selection) + ' selected' ? getSelectionTypeDisplayText(selection) + ' selected'
: `Please select ${arg.multiple ? 'one or more faces' : 'one face'}`} : `Please select ${
arg.multiple ? 'one or more ' : 'one '
}${getSemanticSelectionType(arg.selectionTypes).join(' or ')}`}
<input <input
id="selection" id="selection"
name="selection" name="selection"

View File

@ -187,6 +187,22 @@ const CustomIconMap = {
/> />
</svg> </svg>
), ),
fillet: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 5H5V15H15V12C15 8.13401 11.866 5 8 5ZM5 4H4V5V15V16H5H15H16V15V12C16 7.58172 12.4183 4 8 4H5Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.5 3.5H5.5H8.5C12.9183 3.5 16.5 7.08172 16.5 11.5V14.5V15.5H16V12C16 7.58172 12.4182 4 7.99996 4H4.5V3.5Z"
fill="currentColor"
/>
</svg>
),
file: ( file: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path

View File

@ -33,6 +33,7 @@ import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
import { import {
Selections, Selections,
canExtrudeSelection, canExtrudeSelection,
canFilletSelection,
handleSelectionBatch, handleSelectionBatch,
isSelectionLastLine, isSelectionLastLine,
isRangeInbetweenCharacters, isRangeInbetweenCharacters,
@ -72,6 +73,7 @@ import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -444,6 +446,12 @@ export const ModelingMachineProvider = ({
if (selectionRanges.codeBasedSelections.length <= 0) return false if (selectionRanges.codeBasedSelections.length <= 0) return false
return true return true
}, },
'has valid fillet selection': ({ selectionRanges }) =>
hasValidFilletSelection({
selectionRanges,
ast: kclManager.ast,
code: codeManager.code,
}),
'Selection is on face': ({ selectionRanges }, { data }) => { 'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast)) if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
@ -494,7 +502,6 @@ export const ModelingMachineProvider = ({
kclManager.ast, kclManager.ast,
data.sketchPathToNode, data.sketchPathToNode,
data.extrudePathToNode, data.extrudePathToNode,
kclManager.programMemory,
data.cap data.cap
) )
if (trap(sketched)) return Promise.reject(sketched) if (trap(sketched)) return Promise.reject(sketched)

View File

@ -304,7 +304,6 @@ describe('testing sketchOnExtrudedFace', () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([9.7, 9.19], %)` const segmentSnippet = `line([9.7, 9.19], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -321,8 +320,7 @@ describe('testing sketchOnExtrudedFace', () => {
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded
@ -345,7 +343,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `close(%)` const segmentSnippet = `close(%)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -362,8 +359,7 @@ const sketch001 = startSketchOn(part001, seg01)`)
const extruded = sketchOnExtrudedFace( const extruded = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
const { modifiedAst } = extruded const { modifiedAst } = extruded
@ -386,7 +382,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
|> extrude(5 + 7, %)` |> extrude(5 + 7, %)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const sketchSnippet = `startProfileAt([3.58, 2.06], %)` const sketchSnippet = `startProfileAt([3.58, 2.06], %)`
const sketchRange: [number, number] = [ const sketchRange: [number, number] = [
code.indexOf(sketchSnippet), code.indexOf(sketchSnippet),
@ -404,7 +399,6 @@ const sketch001 = startSketchOn(part001, seg01)`)
ast, ast,
sketchPathToNode, sketchPathToNode,
extrudePathToNode, extrudePathToNode,
programMemory,
'end' 'end'
) )
if (err(extruded)) throw extruded if (err(extruded)) throw extruded
@ -436,7 +430,6 @@ const sketch001 = startSketchOn(part001, 'END')`)
const part001 = extrude(5 + 7, sketch001)` const part001 = extrude(5 + 7, sketch001)`
const ast = parse(code) const ast = parse(code)
if (err(ast)) throw ast if (err(ast)) throw ast
const programMemory = await enginelessExecutor(ast)
const segmentSnippet = `line([4.99, -0.46], %)` const segmentSnippet = `line([4.99, -0.46], %)`
const segmentRange: [number, number] = [ const segmentRange: [number, number] = [
code.indexOf(segmentSnippet), code.indexOf(segmentSnippet),
@ -453,8 +446,7 @@ const sketch001 = startSketchOn(part001, 'END')`)
const updatedAst = sketchOnExtrudedFace( const updatedAst = sketchOnExtrudedFace(
ast, ast,
segmentPathToNode, segmentPathToNode,
extrudePathToNode, extrudePathToNode
programMemory
) )
if (err(updatedAst)) throw updatedAst if (err(updatedAst)) throw updatedAst
const newCode = recast(updatedAst.modifiedAst) const newCode = recast(updatedAst.modifiedAst)

View File

@ -349,7 +349,6 @@ export function sketchOnExtrudedFace(
node: Program, node: Program,
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode, extrudePathToNode: PathToNode,
programMemory: ProgramMemory,
cap: 'none' | 'start' | 'end' = 'none' cap: 'none' | 'start' | 'end' = 'none'
): { modifiedAst: Program; pathToNode: PathToNode } | Error { ): { modifiedAst: Program; pathToNode: PathToNode } | Error {
let _node = { ...node } let _node = { ...node }
@ -388,7 +387,6 @@ export function sketchOnExtrudedFace(
if (cap === 'none') { if (cap === 'none') {
const __tag = addTagForSketchOnFace( const __tag = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory,
pathToNode: sketchPathToNode, pathToNode: sketchPathToNode,
node: _node, node: _node,
}, },

View File

@ -0,0 +1,315 @@
import {
parse,
recast,
initPromise,
PathToNode,
Value,
Program,
CallExpression,
} from '../wasm'
import { addFillet, isTagUsedInFillet } from './addFillet'
import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
import { createLiteral } from 'lang/modifyAst'
import { err } from 'lib/trap'
beforeAll(async () => {
await initPromise // Initialize the WASM environment before running tests
})
const runFilletTest = async (
code: string,
segmentSnippet: string,
extrudeSnippet: string,
radius = createLiteral(5) as Value,
expectedCode: string
) => {
const astOrError = parse(code)
if (astOrError instanceof Error) {
return new Error('AST not found')
}
const ast = astOrError as Program
const segmentRange: [number, number] = [
code.indexOf(segmentSnippet),
code.indexOf(segmentSnippet) + segmentSnippet.length,
]
const pathToSegmentNode: PathToNode = getNodePathFromSourceRange(
ast,
segmentRange
)
const extrudeRange: [number, number] = [
code.indexOf(extrudeSnippet),
code.indexOf(extrudeSnippet) + extrudeSnippet.length,
]
const pathToExtrudeNode: PathToNode = getNodePathFromSourceRange(
ast,
extrudeRange
)
if (pathToExtrudeNode instanceof Error) {
return new Error('Path to extrude node not found')
}
// const radius = createLiteral(5) as Value
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
if (result instanceof Error) {
return result
}
const { modifiedAst } = result
const newCode = recast(modifiedAst)
expect(newCode).toContain(expectedCode)
}
describe('Testing addFillet', () => {
/**
* 1. Ideal Case
*/
it('should add a fillet to a specific segment after extrusion, clean', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 2. Case of existing tag in the other line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in any other line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg02)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg01)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg02] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 3. Case of existing tag in the fillet line
*/
it('should add a fillet to a specific segment after extrusion with existing tag in that exact line', async () => {
const code = `
const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
`
const segmentSnippet = `line([-87.24, -47.08], %, $seg03)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 5, tags: [seg03] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
/**
* 4. Case of existing fillet on some other segment
*/
it('should add another fillet after the existing fillet', async () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)`
const segmentSnippet = `line([60.04, -55.72], %)`
const extrudeSnippet = `const extrude001 = extrude(50, sketch001)`
const radius = createLiteral(5) as Value
const expectedCode = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([2.16, 49.67], %)
|> line([101.49, 139.93], %)
|> line([60.04, -55.72], %, $seg01)
|> line([1.29, -115.74], %)
|> line([-87.24, -47.08], %, $seg03)
|> tangentialArcTo([56.15, -94.58], %)
|> tangentialArcTo([14.68, -104.52], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(50, sketch001)
|> fillet({ radius: 10, tags: [seg03] }, %)
|> fillet({ radius: 5, tags: [seg01] }, %)`
await runFilletTest(
code,
segmentSnippet,
extrudeSnippet,
radius,
expectedCode
)
})
})
describe('Testing isTagUsedInFillet', () => {
const code = `const sketch001 = startSketchOn('XZ')
|> startProfileAt([7.72, 4.13], %)
|> line([7.11, 3.48], %, $seg01)
|> line([-3.29, -13.85], %)
|> line([-6.37, 3.88], %, $seg02)
|> close(%)
const extrude001 = extrude(-5, sketch001)
|> fillet({
radius: 1.11,
tags: [
getOppositeEdge(seg01, %),
seg01,
getPreviousAdjacentEdge(seg02, %)
]
}, %)
`
it('should correctly identify getOppositeEdge and baseEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([7.11, 3.48], %, $seg01)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getOppositeEdge', 'baseEdge'])
})
it('should correctly identify getPreviousAdjacentEdge edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-6.37, 3.88], %, $seg02)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual(['getPreviousAdjacentEdge'])
})
it('should correctly identify no edges', () => {
const ast = parse(code)
if (err(ast)) return
const lineOfInterest = `line([-3.29, -13.85], %)`
const range: [number, number] = [
code.indexOf(lineOfInterest),
code.indexOf(lineOfInterest) + lineOfInterest.length,
]
const pathToNode = getNodePathFromSourceRange(ast, range)
if (err(pathToNode)) return
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToNode,
'CallExpression'
)
if (err(callExp)) return
const edges = isTagUsedInFillet({ ast, callExp: callExp.node })
expect(edges).toEqual([])
})
})

View File

@ -0,0 +1,405 @@
import {
ArrayExpression,
CallExpression,
ObjectExpression,
PathToNode,
Program,
Value,
VariableDeclaration,
VariableDeclarator,
} from '../wasm'
import {
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
createObjectExpression,
createArrayExpression,
createIdentifier,
createPipeExpression,
} from '../modifyAst'
import {
getNodeFromPath,
getNodePathFromSourceRange,
hasSketchPipeBeenExtruded,
traverse,
} from '../queryAst'
import {
addTagForSketchOnFace,
getTagFromCallExpression,
sketchLineHelperMap,
} from '../std/sketch'
import { err } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
// import { forEach } from 'jszip'
export function addFillet(
node: Program,
pathToSegmentNode: PathToNode,
pathToExtrudeNode: PathToNode,
radius = createLiteral(5) as Value
// shouldPipe = false, // TODO: Implement this feature
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
// close ast to make mutations safe
let _node: Program = JSON.parse(JSON.stringify(node))
/**
* Add Tag to the Segment Expression
*/
// Find the specific sketch segment to tag with the new tag
const sketchSegmentChunk = getNodeFromPath(
_node,
pathToSegmentNode,
'CallExpression'
)
if (err(sketchSegmentChunk)) return sketchSegmentChunk
const { node: sketchSegmentNode } = sketchSegmentChunk as {
node: CallExpression
}
// Check whether selection is a valid segment from sketchLineHelpersMap
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
return new Error('Selection is not a sketch segment')
}
// Add tag to the sketch segment or use existing tag
const taggedSegment = addTagForSketchOnFace(
{
// previousProgramMemory: programMemory,
pathToNode: pathToSegmentNode,
node: _node,
},
sketchSegmentNode.callee.name
)
if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment
/**
* Find Extrude Expression automatically
*/
// 1. Get the sketch name
/**
* Add Fillet to the Extrude expression
*/
// Create the fillet call expression in one line
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radius,
tags: createArrayExpression([createIdentifier(tag)]),
}),
createPipeSubstitution(),
])
// Locate the extrude call
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
_node,
pathToExtrudeNode,
'VariableDeclaration'
)
if (err(extrudeChunk)) return extrudeChunk
const { node: extrudeVarDecl } = extrudeChunk
const extrudeDeclarator = extrudeVarDecl.declarations[0]
const extrudeInit = extrudeDeclarator.init
if (
!extrudeDeclarator ||
(extrudeInit.type !== 'CallExpression' &&
extrudeInit.type !== 'PipeExpression')
) {
return new Error('Extrude PipeExpression / CallExpression not found.')
}
// determine if extrude is in a PipeExpression or CallExpression
// CallExpression - no fillet
// PipeExpression - fillet exists
const getPathToNodeOfFilletLiteral = (
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
): PathToNode => {
let pathToFilletObj: any
let inFillet = false
traverse(extrudeDeclarator.init, {
enter(node, path) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
const hasTag = node.properties.some((prop) => {
const isTagProp = prop.key.name === 'tags'
if (isTagProp && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag
)
}
return false
})
if (!hasTag) return false
pathToFilletObj = path
node.properties.forEach((prop, index) => {
if (prop.key.name === 'radius') {
pathToFilletObj.push(
['properties', 'ObjectExpression'],
[index, 'index'],
['value', 'Property']
)
}
})
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
(path) => path[1] === 'PipeExpression'
)
indexOfPipeExpression =
indexOfPipeExpression === -1
? pathToExtrudeNode.length
: indexOfPipeExpression
return [
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
...pathToFilletObj,
]
}
if (extrudeInit.type === 'CallExpression') {
// 1. no fillet case
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
} else if (extrudeInit.type === 'PipeExpression') {
// 2. fillet case
// there are 2 options here:
const existingFilletCall = extrudeInit.body.find((node) => {
return node.type === 'CallExpression' && node.callee.name === 'fillet'
})
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
return new Error('Fillet CallExpression not found.')
}
// check if the existing fillet has the same tag as the new fillet
let filletTag = null
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
.properties
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
const elements = (tagsProperty.value as ArrayExpression).elements
if (elements.length > 0 && elements[0].type === 'Identifier') {
filletTag = elements[0].name
}
}
} else {
return new Error('Expected an ObjectExpression node')
}
if (filletTag !== tag) {
extrudeInit.body.push(filletCall)
return {
modifiedAst: _node,
pathToFilletNode: getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tag
),
}
}
} else {
return new Error('Unsupported extrude type.')
}
return new Error('Unsupported extrude type.')
}
export const hasValidFilletSelection = ({
selectionRanges,
ast,
code,
}: {
selectionRanges: Selections
ast: Program
code: string
}) => {
// case 0: check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'extrude') {
extrudeExists = true
}
},
})
if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
}
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
return false
}
}
return canFilletSelection(selectionRanges)
}
type EdgeTypes =
| 'baseEdge'
| 'getNextAdjacentEdge'
| 'getPreviousAdjacentEdge'
| 'getOppositeEdge'
export const isTagUsedInFillet = ({
ast,
callExp,
}: {
ast: Program
callExp: CallExpression
}): Array<EdgeTypes> => {
const tag = getTagFromCallExpression(callExp)
if (err(tag)) return []
let inFillet = false
let inObj = false
let inTagHelper: EdgeTypes | '' = ''
const edges: Array<EdgeTypes> = []
traverse(ast, {
enter: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = node.callee.name
}
if (
inObj &&
inFillet &&
!inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push('baseEdge')
}
if (
inObj &&
inFillet &&
inTagHelper &&
node.type === 'Identifier' &&
node.name === tag
) {
edges.push(inTagHelper)
}
},
leave: (node) => {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
if (inFillet && node.type === 'ObjectExpression') {
node.properties.forEach((prop) => {
if (
prop.key.name === 'tags' &&
prop.value.type === 'ArrayExpression'
) {
inObj = true
}
})
}
if (
inObj &&
inFillet &&
node.type === 'CallExpression' &&
(node.callee.name === 'getOppositeEdge' ||
node.callee.name === 'getNextAdjacentEdge' ||
node.callee.name === 'getPreviousAdjacentEdge')
) {
inTagHelper = ''
}
},
})
return edges
}

View File

@ -221,7 +221,7 @@ describe('testing addTagForSketchOnFace', () => {
const pathToNode = getNodePathFromSourceRange(ast, sourceRange) const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
const sketchOnFaceRetVal = addTagForSketchOnFace( const sketchOnFaceRetVal = addTagForSketchOnFace(
{ {
previousProgramMemory: programMemory, // previousProgramMemory: programMemory, // redundant?
pathToNode, pathToNode,
node: ast, node: ast,
}, },

View File

@ -28,7 +28,6 @@ import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import { import {
SketchLineHelper, SketchLineHelper,
ModifyAstBase,
TransformCallback, TransformCallback,
ConstrainInfo, ConstrainInfo,
RawValues, RawValues,
@ -37,6 +36,7 @@ import {
SingleValueInput, SingleValueInput,
VarValueKeys, VarValueKeys,
ArrayOrObjItemInput, ArrayOrObjItemInput,
AddTagInfo,
} from 'lang/std/stdTypes' } from 'lang/std/stdTypes'
import { import {
@ -308,6 +308,18 @@ function singleRawValueHelper(
] ]
} }
function getTag(index = 2): SketchLineHelper['getTag'] {
return (callExp: CallExpression) => {
if (callExp.type !== 'CallExpression')
return new Error('Not a CallExpression')
const arg = callExp.arguments?.[index]
if (!arg) return new Error('No argument')
if (arg.type !== 'TagDeclarator')
return new Error('Tag not a TagDeclarator')
return arg.value
}
}
export const lineTo: SketchLineHelper = { export const lineTo: SketchLineHelper = {
add: ({ add: ({
node, node,
@ -377,6 +389,7 @@ export const lineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -503,6 +516,7 @@ export const line: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -563,6 +577,7 @@ export const xLineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -623,6 +638,7 @@ export const yLineTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -682,6 +698,7 @@ export const xLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -738,6 +755,7 @@ export const yLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
horzVertConstraintInfoHelper( horzVertConstraintInfoHelper(
@ -830,6 +848,7 @@ export const tangentialArcTo: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
@ -948,6 +967,7 @@ export const angledLine: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1044,6 +1064,7 @@ export const angledLineOfXLength: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1140,6 +1161,7 @@ export const angledLineOfYLength: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1227,6 +1249,7 @@ export const angledLineToX: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1316,6 +1339,7 @@ export const angledLineToY: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp, ...args) => getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper( commonConstraintInfoHelper(
@ -1440,6 +1464,7 @@ export const angledLineThatIntersects: SketchLineHelper = {
pathToNode, pathToNode,
} }
}, },
getTag: getTag(),
addTag: addTag(), addTag: addTag(),
getConstraintInfo: (callExp: CallExpression, code, pathToNode) => { getConstraintInfo: (callExp: CallExpression, code, pathToNode) => {
if (callExp.type !== 'CallExpression') return [] if (callExp.type !== 'CallExpression') return []
@ -1792,10 +1817,7 @@ export function replaceSketchLine({
return { modifiedAst, valueUsedInTransform, pathToNode } return { modifiedAst, valueUsedInTransform, pathToNode }
} }
export function addTagForSketchOnFace( export function addTagForSketchOnFace(a: AddTagInfo, expressionName: string) {
a: ModifyAstBase,
expressionName: string
) {
if (expressionName === 'close') { if (expressionName === 'close') {
return addTag(1)(a) return addTag(1)(a)
} }
@ -1806,6 +1828,17 @@ export function addTagForSketchOnFace(
return new Error(`"${expressionName}" is not a sketch line helper`) return new Error(`"${expressionName}" is not a sketch line helper`)
} }
export function getTagFromCallExpression(
callExp: CallExpression
): string | Error {
if (callExp.callee.name === 'close') return getTag(1)(callExp)
if (callExp.callee.name in sketchLineHelperMap) {
const { getTag } = sketchLineHelperMap[callExp.callee.name]
return getTag(callExp)
}
return new Error(`"${callExp.callee.name}" is not a sketch line helper`)
}
function isAngleLiteral(lineArugement: Value): boolean { function isAngleLiteral(lineArugement: Value): boolean {
return lineArugement?.type === 'ArrayExpression' return lineArugement?.type === 'ArrayExpression'
? isLiteralArrayOrStatic(lineArugement.elements[0]) ? isLiteralArrayOrStatic(lineArugement.elements[0])
@ -1816,9 +1849,7 @@ function isAngleLiteral(lineArugement: Value): boolean {
: false : false
} }
type addTagFn = ( type addTagFn = (a: AddTagInfo) => { modifiedAst: Program; tag: string } | Error
a: ModifyAstBase
) => { modifiedAst: Program; tag: string } | Error
function addTag(tagIndex = 2): addTagFn { function addTag(tagIndex = 2): addTagFn {
return ({ node, pathToNode }) => { return ({ node, pathToNode }) => {

View File

@ -32,6 +32,11 @@ export interface ModifyAstBase {
pathToNode: PathToNode pathToNode: PathToNode
} }
export interface AddTagInfo {
node: Program
pathToNode: PathToNode
}
interface addCall extends ModifyAstBase { interface addCall extends ModifyAstBase {
to: [number, number] to: [number, number]
from: [number, number] from: [number, number]
@ -127,7 +132,8 @@ export interface SketchLineHelper {
pathToNode: PathToNode pathToNode: PathToNode
} }
| Error | Error
addTag: (a: ModifyAstBase) => getTag: (a: CallExpression) => string | Error
addTag: (a: AddTagInfo) =>
| { | {
modifiedAst: Program modifiedAst: Program
tag: string tag: string

View File

@ -27,6 +27,11 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number] // result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue distance: KclCommandValue
} }
Fillet: {
// todo
selection: Selections
radius: KclCommandValue
}
'change tool': { 'change tool': {
tool: SketchTool tool: SketchTool
} }
@ -185,4 +190,36 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Fillet: {
// todo
description: 'Fillet edge',
icon: 'fillet',
needsReview: true,
args: {
selection: {
inputType: 'selection',
selectionTypes: [
'default',
'line-end',
'line-mid',
'extrude-wall', // to fix: accespts only this selection type
'start-cap',
'end-cap',
'point',
'edge',
'line',
'arc',
'all',
],
multiple: true, // TODO: multiple selection like in extrude command
required: true,
skip: true,
},
radius: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
} }

View File

@ -51,6 +51,7 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
export const KCL_DEFAULT_CONSTANT_PREFIXES = { export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch', SKETCH: 'sketch',
EXTRUDE: 'extrude', EXTRUDE: 'extrude',
SEGMENT: 'seg',
} as const } as const
/** The default KCL length expression */ /** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5` export const KCL_DEFAULT_LENGTH = `5`

View File

@ -406,6 +406,17 @@ export function canExtrudeSelection(selection: Selections) {
) )
} }
export function canFilletSelection(selection: Selections) {
const commonNodes = selection.codeBasedSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
) // TODO FILLET DUMMY PLACEHOLDER
return (
!!isSketchPipe(selection) &&
commonNodes.every((n) => nodeHasClose(n)) &&
commonNodes.every((n) => !nodeHasExtrude(n))
)
}
function canExtrudeSelectionItem(selection: Selections, i: number) { function canExtrudeSelectionItem(selection: Selections, i: number) {
const commonNode = buildCommonNodeFromSelection(selection, i) const commonNode = buildCommonNodeFromSelection(selection, i)

View File

@ -37,4 +37,7 @@ if (typeof window !== 'undefined') {
document.addEventListener('mousemove', (e) => document.addEventListener('mousemove', (e) =>
console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`) console.log(`await page.mouse.click(${e.clientX}, ${e.clientY})`)
) )
;(window as any).enableFillet = () => {
;(window as any)._enableFillet = true
}
} }

View File

@ -6,7 +6,7 @@ import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
import { Value, parse } from 'lang/wasm' import { Value, parse } from 'lang/wasm'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { executeAst } from 'lang/langHelpers' import { executeAst } from 'lang/langHelpers'
import { trap } from 'lib/trap' import { err, trap } from 'lib/trap'
const isValidVariableName = (name: string) => const isValidVariableName = (name: string) =>
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
@ -86,6 +86,7 @@ export function useCalculateKclExpression({
const execAstAndSetResult = async () => { const execAstAndSetResult = async () => {
const _code = `const __result__ = ${value}` const _code = `const __result__ = ${value}`
const ast = parse(_code) const ast = parse(_code)
if (err(ast)) return
if (trap(ast, { suppress: true })) return if (trap(ast, { suppress: true })) return
const _programMem: any = { root: {}, return: null } const _programMem: any = { root: {}, return: null }

View File

@ -38,6 +38,7 @@ import {
deleteFromSelection, deleteFromSelection,
extrudeSketch, extrudeSketch,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { addFillet } from 'lang/modifyAst/addFillet'
import { getNodeFromPath } from '../lang/queryAst' import { getNodeFromPath } from '../lang/queryAst'
import { import {
applyConstraintEqualAngle, applyConstraintEqualAngle,
@ -207,6 +208,7 @@ export type ModelingMachineEvent =
| { type: 'Re-execute' } | { type: 'Re-execute' }
| { type: 'Export'; data: ModelingCommandSchema['Export'] } | { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] } | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
| { | {
type: 'Add rectangle origin' type: 'Add rectangle origin'
data: [x: number, y: number] data: [x: number, y: number]
@ -317,6 +319,13 @@ export const modelingMachine = createMachine(
internal: true, internal: true,
}, },
Fillet: {
target: 'idle',
cond: 'has valid fillet selection', // TODO: fix selections
actions: ['AST fillet'],
internal: true,
},
Export: { Export: {
target: 'idle', target: 'idle',
internal: true, internal: true,
@ -1102,6 +1111,71 @@ export const modelingMachine = createMachine(
await kclManager.updateAst(modifiedAst, true) await kclManager.updateAst(modifiedAst, true)
}, },
'AST fillet': async (_, event) => {
if (!event.data) return
const { selection, radius } = event.data
let ast = kclManager.ast
if (
'variableName' in radius &&
radius.variableName &&
radius.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
ast.body = newBody
}
const pathToSegmentNode = getNodePathFromSourceRange(
ast,
selection.codeBasedSelections[0].range
)
const varDecNode = getNodeFromPath<VariableDeclaration>(
ast,
pathToSegmentNode,
'VariableDeclaration'
)
if (err(varDecNode)) return
const sketchVar = varDecNode.node.declarations[0].id.name
const sketchGroup = kclManager.programMemory.root[sketchVar]
if (sketchGroup.type !== 'SketchGroup') return
const idArtifact = engineCommandManager.artifactMap[sketchGroup.id]
if (idArtifact.commandType !== 'start_path') return
const extrusionArtifactId = (idArtifact as any)?.extrusions?.[0]
if (typeof extrusionArtifactId !== 'string') return
const extrusionArtifact = (engineCommandManager.artifactMap as any)[
extrusionArtifactId
]
if (!extrusionArtifact) return
const pathToExtrudeNode = getNodePathFromSourceRange(
ast,
extrusionArtifact.range
)
// we assume that there is only one body related to the sketch
// and apply the fillet to it
const addFilletResult = addFillet(
ast,
pathToSegmentNode,
pathToExtrudeNode,
'variableName' in radius
? radius.variableIdentifierAst
: radius.valueAst
)
if (trap(addFilletResult)) return
const { modifiedAst, pathToFilletNode } = addFilletResult
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: pathToFilletNode,
})
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
},
'conditionally equip line tool': (_, { type }) => { 'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') { if (type === 'done.invoke.animate-to-face') {
sceneInfra.modelingSend({ sceneInfra.modelingSend({