Fillet UI (#2718)
* 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>
@ -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,
|
||||||
}) => {
|
}) => {
|
||||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
315
src/lang/modifyAst/addFillet.test.ts
Normal 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([])
|
||||||
|
})
|
||||||
|
})
|
405
src/lang/modifyAst/addFillet.ts
Normal 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
|
||||||
|
}
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -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`
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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({
|
||||||
|