Compare commits

...

12 Commits

17 changed files with 1327 additions and 67 deletions

View File

@ -0,0 +1,298 @@
import type { Page, Locator } from '@playwright/test'
import { expect, test as base } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import { uuidv4 } from 'lib/utils'
type CmdBarSerilised =
| {
stage: 'commandBarClosed'
// TODO no more properties needed but needs to be implemented in _serialiseCmdBar
}
| {
stage: 'pickCommand'
// TODO this will need more properties when implemented in _serialiseCmdBar
}
| {
stage: 'arguments'
currentArgKey: string
currentArgValue: string
headerArguments: Record<string, string>
highlightedHeaderArg: string
commandName: string
}
| {
stage: 'review'
headerArguments: Record<string, string>
commandName: string
}
export class AuthenticatedApp {
private readonly exeIndicator: Locator
private readonly diagnosticsTooltip: Locator
private readonly diagnosticsGutterIcon: Locator
private readonly codeContent: Locator
private readonly extrudeButton: Locator
readonly startSketchBtn: Locator
readonly rectangleBtn: Locator
readonly exitSketchBtn: Locator
u: Awaited<ReturnType<typeof getUtils>>
constructor(public readonly page: Page) {
this.codeContent = page.locator('.cm-content')
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
// this.diagnosticsTooltip = page.locator('.cm-tooltip')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
this.u = {} as any
}
async initialise(code = '') {
const u = await getUtils(this.page)
this.u = u
await this.page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
'utf-8'
)
}
makeMouseHelpers = (
x: number,
y: number,
{ steps }: { steps: number } = { steps: 5000 }
) => [
() => this.page.mouse.click(x, y),
() => this.page.mouse.move(x, y, { steps }),
]
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
*
* Expects the viewPort to be 1000x500 */
clickNoWhere = () => this.page.mouse.click(998, 60)
// Toolbars
expectExtrudeButtonToBeDisabled = async () =>
await expect(this.extrudeButton).toBeDisabled()
expectExtrudeButtonToBeEnabled = async () =>
await expect(this.extrudeButton).not.toBeDisabled()
clickExtrudeButton = async () => await this.extrudeButton.click()
private _serialiseCmdBar = async (): Promise<CmdBarSerilised> => {
const reviewForm = await this.page.locator('#review-form')
const getHeaderArgs = async () => {
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
const entries = await Promise.all(
inputs.map((input) => {
const key = input
.locator('[data-test-name="arg-name"]')
.innerText()
.then((a) => a.trim())
const value = input
.getByTestId('header-arg-value')
.innerText()
.then((a) => a.trim())
return Promise.all([key, value])
})
)
return Object.fromEntries(entries)
}
const getCommandName = () =>
this.page.getByTestId('command-name').textContent()
if (await reviewForm.isVisible()) {
const [headerArguments, commandName] = await Promise.all([
getHeaderArgs(),
getCommandName(),
])
return {
stage: 'review',
headerArguments,
commandName: commandName || '',
}
}
const [
currentArgKey,
currentArgValue,
headerArguments,
highlightedHeaderArg,
commandName,
] = await Promise.all([
this.page.getByTestId('cmd-bar-arg-name').textContent(),
this.page.getByTestId('cmd-bar-arg-value').textContent(),
getHeaderArgs(),
this.page
.locator('[data-is-current-arg="true"]')
.locator('[data-test-name="arg-name"]')
.textContent(),
getCommandName(),
])
return {
stage: 'arguments',
currentArgKey: currentArgKey || '',
currentArgValue: currentArgValue || '',
headerArguments,
highlightedHeaderArg: highlightedHeaderArg || '',
commandName: commandName || '',
}
}
expectCmdBarToBe = async (expected: CmdBarSerilised) => {
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
}
progressCmdBar = async () => {
if (Math.random() > 0.5) {
const arrowButton = this.page.getByRole('button', {
name: 'arrow right Continue',
})
if (await arrowButton.isVisible()) {
await arrowButton.click()
} else {
await this.page
.getByRole('button', { name: 'checkmark Submit command' })
.click()
}
} else {
await this.page.keyboard.press('Enter')
}
}
expectCodeHighlightedToBe = async (
code: string,
{ timeout }: { timeout: number } = { timeout: 5000 }
) =>
await expect
.poll(
async () => {
const texts = (
await this.page.getByTestId('hover-highlight').allInnerTexts()
).map((s) => s.replace(/\s+/g, '').trim())
return texts.join('')
},
{ timeout }
)
.toBe(code.replace(/\s+/g, '').trim())
expectActiveLinesToBe = async (lines: Array<string>) => {
await expect
.poll(async () => {
return (await this.page.locator('.cm-activeLine').allInnerTexts()).map(
(l) => l.trim()
)
})
.toEqual(lines.map((l) => l.trim()))
}
private _expectEditorToContain =
(not = false) =>
(
code: string,
{
shouldNormalise = false,
timeout = 5_000,
}: { shouldNormalise?: boolean; timeout?: number } = {}
) => {
if (!shouldNormalise) {
const expectStart = expect(this.codeContent)
if (not) {
return expectStart.not.toContainText(code, { timeout })
}
return expectStart.toContainText(code, { timeout })
}
const normalisedCode = code.replaceAll(/\s+/g, '').trim()
const expectStart = expect.poll(
async () => {
const editorText = await this.codeContent.textContent()
return editorText?.replaceAll(/\s+/g, '').trim()
},
{
timeout,
}
)
if (not) {
return expectStart.not.toContain(normalisedCode)
}
return expectStart.toContain(normalisedCode)
}
expectEditor = {
toContain: this._expectEditorToContain(),
not: { toContain: this._expectEditorToContain(true) },
}
moveCameraTo = async (
pos: { x: number; y: number; z: number },
target: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }
) => {
await this.u.openAndClearDebugPanel()
await this.u.doAndWaitForImageDiff(
() =>
this.u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: pos,
center: target,
up: { x: 0, y: 0, z: 1 },
},
}),
300
)
await this.u.closeDebugPanel()
}
waitForExecutionDone = async () => {
await expect(this.exeIndicator).toBeVisible()
}
private _serialiseDiagnostics = async (): Promise<Array<string>> => {
const diagnostics = await this.diagnosticsGutterIcon.all()
const diagnosticsContent: string[] = []
for (const diag of diagnostics) {
await diag.hover()
// await expect(this.diagnosticsTooltip)
const content = await this.diagnosticsTooltip.allTextContents()
diagnosticsContent.push(content.join(''))
}
return [...new Set(diagnosticsContent)].map((d) => d.trim())
}
expectDiagnosticsToBe = async (expected: Array<string>) =>
await expect
.poll(async () => {
const result = await this._serialiseDiagnostics()
return result
})
.toEqual(expected.map((e) => e.trim()))
startSketchPlaneSelection = async () =>
this.u.doAndWaitForImageDiff(() => this.startSketchBtn.click(), 500)
}
export const test = base.extend<{
app: AuthenticatedApp
}>({
app: async ({ page }, use) => {
const authenticatedApp = new AuthenticatedApp(page)
await use(authenticatedApp)
},
})
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -0,0 +1,312 @@
import { test, expect, AuthenticatedApp } from './authenticatedAppFixture'
// test file is for testing point an click code gen functionality that's not sketch mode related
test.describe('verify sketch on chamfer works', () => {
const _sketchOnAChamfer =
(app: AuthenticatedApp) =>
async ({
clickCoords,
cameraPos,
cameraTarget,
beforeChamferSnippet,
afterChamferSelectSnippet,
afterRectangle1stClickSnippet,
afterRectangle2ndClickSnippet,
}: {
clickCoords: { x: number; y: number }
cameraPos: { x: number; y: number; z: number }
cameraTarget: { x: number; y: number; z: number }
beforeChamferSnippet: string
afterChamferSelectSnippet: string
afterRectangle1stClickSnippet: string
afterRectangle2ndClickSnippet: string
}) => {
const [clickChamfer] = app.makeMouseHelpers(clickCoords.x, clickCoords.y)
const [rectangle1stClick] = app.makeMouseHelpers(573, 149)
const [rectangle2ndClick, rectangle2ndMove] = app.makeMouseHelpers(
598,
380,
{ steps: 5 }
)
await app.moveCameraTo(cameraPos, cameraTarget)
await test.step('check chamfer selection changes cursor positon', async () => {
await expect(async () => {
// sometimes initial click doesn't register
await clickChamfer()
await app.expectActiveLinesToBe([beforeChamferSnippet.slice(-5)])
}).toPass({ timeout: 40_000, intervals: [500] })
})
await test.step('starting a new and selecting a chamfer should animate to the new sketch and possible break up the initial chamfer if it had one than more tag', async () => {
await app.startSketchPlaneSelection()
await clickChamfer()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await app.expectEditor.toContain(afterChamferSelectSnippet)
})
await test.step('make sure a basic sketch can be added', async () => {
await app.rectangleBtn.click()
await rectangle1stClick()
await app.expectEditor.toContain(afterRectangle1stClickSnippet)
await app.u.doAndWaitForImageDiff(() => rectangle2ndMove(), 50)
await rectangle2ndClick()
await app.expectEditor.toContain(afterRectangle2ndClickSnippet)
})
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
await app.exitSketchBtn.click()
await app.waitForExecutionDone()
})
await test.step('Check there is no errors after code created in previous steps executes', async () => {
await app.expectDiagnosticsToBe([])
})
}
test('works on all edge selections and can break up multi edges in a chamfer array', async ({
app,
}) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
const file = await app.getInputFile('e2e-can-sketch-on-chamfer.kcl')
await app.initialise(file)
const sketchOnAChamfer = _sketchOnAChamfer(app)
await sketchOnAChamfer({
clickCoords: { x: 570, y: 220 },
cameraPos: { x: 16020, y: -2000, z: 10500 },
cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)
chamfer({length:30,tags:[
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02),
getOppositeEdge(seg01)
]}, %)`,
afterChamferSelectSnippet:
'const sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await sketchOnAChamfer({
clickCoords: { x: 690, y: 250 },
cameraPos: { x: 16020, y: -2000, z: 10500 },
cameraTarget: { x: -150, y: -4500, z: -80 },
beforeChamferSnippet: `angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)chamfer({
length: 30,
tags: [
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02)
]
}, %)`,
afterChamferSelectSnippet:
'const sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 },
cameraTarget: { x: 8300, y: 1100, z: 4800 },
beforeChamferSnippet: `angledLine([0, 268.43], %, $rectangleSegmentA001)chamfer({
length: 30,
tags: [
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02)
]
}, %)`,
afterChamferSelectSnippet:
'const sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
/// last one
await sketchOnAChamfer({
clickCoords: { x: 620, y: 300 },
cameraPos: { x: -1100, y: -7700, z: 1600 },
cameraTarget: { x: 1450, y: 670, z: 4000 },
beforeChamferSnippet: `chamfer({
length: 30,
tags: [getNextAdjacentEdge(yo)]
}, %)`,
afterChamferSelectSnippet:
'const sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await test.step('verif at the end of the test that final code is what is expected', async () => {
await app.expectEditor.toContain(
`const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
|> chamfer({
length: 30,
tags: [getOppositeEdge(seg01)]
}, %, $seg03)
|> chamfer({ length: 30, tags: [seg01] }, %, $seg04)
|> chamfer({
length: 30,
tags: [getNextAdjacentEdge(seg02)]
}, %, $seg05)
|> chamfer({
length: 30,
tags: [getNextAdjacentEdge(yo)]
}, %, $seg06)
const sketch005 = startSketchOn(extrude001, seg06)
|> startProfileAt([-23.43, 19.69], %)
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch004 = startSketchOn(extrude001, seg05)
|> startProfileAt([82.57, 322.96], %)
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) - 90,
103.07
], %, $rectangleSegmentB003)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %, $rectangleSegmentC003)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch003 = startSketchOn(extrude001, seg04)
|> startProfileAt([-209.64, 255.28], %)
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const sketch002 = startSketchOn(extrude001, seg03)
|> startProfileAt([205.96, 254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`,
{ shouldNormalise: true }
)
})
})
test('works on chamfers on sketchOnFace extrudes', async ({ app, page }) => {
test.skip(
process.platform === 'win32',
'Fails on windows in CI, can not be replicated locally on windows.'
)
const file = await app.getInputFile(
'e2e-can-sketch-on-sketchOnFace-chamfers.kcl'
)
await app.initialise(file)
const sketchOnAChamfer = _sketchOnAChamfer(app)
// clickCoords: { x: 627, y: 287 },
await sketchOnAChamfer({
clickCoords: { x: 858, y: 194 },
cameraPos: { x: 8822, y: 1223, z: 9140 },
cameraTarget: { x: 10856, y: -7390, z: 2832 },
beforeChamferSnippet: `chamfer({
length: 18,
tags: [getNextAdjacentEdge(seg01), seg02]
}, %)`,
afterChamferSelectSnippet:
'const sketch005 = startSketchOn(extrude004, seg05)',
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
})
await page.waitForTimeout(100)
})
})

View File

@ -477,7 +477,9 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
await expect(page.getByText('Unable to delete part')).toBeVisible()
})
test('Hovering over 3d features highlights code', async ({ page }) => {
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engin', async ({
page,
}) => {
const u = await getUtils(page)
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
localStorage.setItem(
@ -542,16 +544,16 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
const close: Coords2d = [720, 200]
const nothing: Coords2d = [600, 200]
const closeEdge: Coords2d = [744, 233]
const closeAdjacentEdge: Coords2d = [688, 123]
const closeAdjacentEdge: Coords2d = [743, 277]
const closeOppositeEdge: Coords2d = [687, 169]
const tangentialArcEdge: Coords2d = [811, 142]
const tangentialArcOppositeEdge: Coords2d = [820, 180]
const tangentialArcAdjacentEdge: Coords2d = [893, 165]
const tangentialArcAdjacentEdge: Coords2d = [688, 123]
const straightSegmentEdge: Coords2d = [819, 369]
const straightSegmentOppositeEdge: Coords2d = [635, 394]
const straightSegmentAdjacentEdge: Coords2d = [679, 329]
const straightSegmentOppositeEdge: Coords2d = [822, 368]
const straightSegmentAdjacentEdge: Coords2d = [893, 165]
await page.mouse.move(nothing[0], nothing[1])
await page.mouse.click(nothing[0], nothing[1])
@ -569,19 +571,27 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
const highlightedLocator = page.getByTestId('hover-highlight')
const activeLineLocator = page.locator('.cm-activeLine')
await test.step(`hover should highlight correct code`, async () => {
await test.step(`hover should highlight correct code, clicking should put the cursor in the right place, and send selection to engine`, async () => {
await expect(async () => {
await page.mouse.move(nothing[0], nothing[1])
await page.mouse.move(coord[0], coord[1])
await expect(highlightedLocator.first()).toBeVisible()
await expect
.poll(async () => {
const textContents = await highlightedLocator.allTextContents()
return textContents.join('').replace(/\s+/g, '')
let textContents = await highlightedLocator.allTextContents()
const textContentsStr = textContents
.join('')
.replace(/\s+/g, '')
console.log(textContentsStr)
return textContentsStr
})
.toBe(highlightCode)
await page.mouse.move(nothing[0], nothing[1])
}).toPass({ timeout: 40_000, intervals: [500] })
})
await test.step(`click should put the cursor in the right place`, async () => {
await expect(highlightedLocator.first()).not.toBeVisible()
// await page.mouse.move(nothing[0], nothing[1], { steps: 5 })
// await expect(highlightedLocator.first()).not.toBeVisible()
await page.mouse.click(coord[0], coord[1])
await expect
.poll(async () => {
@ -689,10 +699,122 @@ const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02)
'angledLineToY({ angle: 30, to: 11.14 }, %)'
)
await checkCodeAtHoverPosition(
'straightSegmentAdjancentEdge',
'straightSegmentAdjacentEdge',
straightSegmentAdjacentEdge,
`angledLineToY({angle:30,to:11.14},%)`,
'angledLineToY({ angle: 30, to: 11.14 }, %)'
`angledLineThatIntersects({angle:3.14,intersectTag:a,offset:0},%)`,
'}, %)'
)
await page.waitForTimeout(200)
await u.removeCurrentCode()
await u.codeLocator.fill(`const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
|> chamfer({
length: 30,
tags: [
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02),
getOppositeEdge(seg01)
]
}, %)
`)
await expect(
page.getByTestId('model-state-indicator-execution-done')
).toBeVisible()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 16118, y: -1654, z: 5855 },
center: { x: 4915, y: -3893, z: 4874 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(nothing[0], nothing[1])
const oppositeChamfer: Coords2d = [577, 230]
const baseChamfer: Coords2d = [726, 258]
const adjacentChamfer1: Coords2d = [653, 99]
const adjacentChamfer2: Coords2d = [653, 430]
await checkCodeAtHoverPosition(
'oppositeChamfer',
oppositeChamfer,
`angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
await checkCodeAtHoverPosition(
'baseChamfer',
baseChamfer,
`angledLine([segAng(rectangleSegmentA001)-90,217.26],%,$seg01)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: -6414, y: 160, z: 6145 },
center: { x: 5919, y: 1236, z: 5251 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(nothing[0], nothing[1])
await checkCodeAtHoverPosition(
'adjacentChamfer1',
adjacentChamfer1,
`lineTo([profileStartX(%),profileStartY(%)],%,$seg02)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
await checkCodeAtHoverPosition(
'adjacentChamfer2',
adjacentChamfer2,
`angledLine([segAng(rectangleSegmentA001),-segLen(rectangleSegmentA001)],%,$yo)chamfer({length:30,tags:[seg01,getNextAdjacentEdge(yo),getNextAdjacentEdge(seg02),getOppositeEdge(seg01)]},%)`,
'}, %)'
)
})
test("Extrude button should be disabled if there's no extrudable geometry when nothing is selected", async ({

View File

@ -605,9 +605,15 @@ export const ModelingMachineProvider = ({
kclManager.ast,
input.sketchPathToNode,
input.extrudePathToNode,
input.cap
input.faceInfo
)
if (trap(sketched)) return Promise.reject(sketched)
if (err(sketched)) {
const sketchedError = new Error(
'Incompatible face, please try another'
)
trap(sketchedError)
return Promise.reject(sketchedError)
}
const { modifiedAst, pathToNode: pathToNewSketchNode } = sketched
await kclManager.executeAstMock(modifiedAst)

View File

@ -11,12 +11,17 @@ import {
getCapCodeRef,
getSweepEdgeCodeRef,
getSweepFromSuspectedSweepSurface,
getEdgeCuteConsumedCodeRef,
getSolid2dCodeRef,
getWallCodeRef,
getArtifactOfTypes,
SegmentArtifact,
} from 'lang/std/artifactGraph'
import { err, reportRejection } from 'lib/trap'
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import { CallExpression } from 'lang/wasm'
import { EdgeCutInfo, ExtrudeFacePlane } from 'machines/modelingMachine'
export function useEngineConnectionSubscriptions() {
const { send, context, state } = useModelingContext()
@ -72,6 +77,17 @@ export function useEngineConnectionSubscriptions() {
editorManager.setHighlightRange([
artifact?.codeRef?.range || [0, 0],
])
} else if (artifact?.type === 'edgeCut') {
const codeRef = artifact.codeRef
const consumedCodeRef = getEdgeCuteConsumedCodeRef(
artifact,
engineCommandManager.artifactGraph
)
editorManager.setHighlightRange(
err(consumedCodeRef)
? [codeRef.range]
: [codeRef.range, consumedCodeRef.range]
)
} else {
editorManager.setHighlightRange([[0, 0]])
}
@ -177,12 +193,21 @@ export function useEngineConnectionSubscriptions() {
engineCommandManager.artifactGraph
)
if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
if (
artifact?.type !== 'cap' &&
artifact?.type !== 'wall' &&
!(
artifact?.type === 'edgeCut' && artifact.subType === 'chamfer'
)
)
return
const codeRef =
artifact.type === 'cap'
? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
: getWallCodeRef(artifact, engineCommandManager.artifactGraph)
: artifact.type === 'wall'
? getWallCodeRef(artifact, engineCommandManager.artifactGraph)
: artifact.codeRef
const faceInfo = await getFaceDetails(faceId)
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
@ -193,6 +218,72 @@ export function useEngineConnectionSubscriptions() {
err(codeRef) ? [0, 0] : codeRef.range
)
const getEdgeCutMeta = (): null | EdgeCutInfo => {
let chamferInfo: {
segment: SegmentArtifact
type: EdgeCutInfo['subType']
} | null = null
if (
artifact?.type === 'edgeCut' &&
artifact.subType === 'chamfer'
) {
const consumedArtifact = getArtifactOfTypes(
{
key: artifact.consumedEdgeId,
types: ['segment', 'sweepEdge'],
},
engineCommandManager.artifactGraph
)
if (err(consumedArtifact)) return null
if (consumedArtifact.type === 'segment') {
chamferInfo = {
type: 'base',
segment: consumedArtifact,
}
} else {
const segment = getArtifactOfTypes(
{ key: consumedArtifact.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(segment)) return null
chamferInfo = {
type: consumedArtifact.subType,
segment,
}
}
}
if (!chamferInfo) return null
const segmentCallExpr = getNodeFromPath<CallExpression>(
kclManager.ast,
chamferInfo?.segment.codeRef.pathToNode || [],
'CallExpression'
)
if (err(segmentCallExpr)) return null
if (segmentCallExpr.node.type !== 'CallExpression') return null
const sketchNodeArgs = segmentCallExpr.node.arguments
const tagDeclarator = sketchNodeArgs.find(
({ type }) => type === 'TagDeclarator'
)
if (!tagDeclarator || tagDeclarator.type !== 'TagDeclarator')
return null
return {
type: 'edgeCut',
subType: chamferInfo.type,
tagName: tagDeclarator.value,
}
}
const edgeCutMeta = getEdgeCutMeta()
const _faceInfo: ExtrudeFacePlane['faceInfo'] = edgeCutMeta
? edgeCutMeta
: artifact.type === 'cap'
? {
type: 'cap',
subType: artifact.subType,
}
: { type: 'wall' }
const extrudePathToNode = !err(extrusion)
? getNodePathFromSourceRange(
kclManager.ast,
@ -211,7 +302,7 @@ export function useEngineConnectionSubscriptions() {
) as [number, number, number],
sketchPathToNode,
extrudePathToNode,
cap: artifact.type === 'cap' ? artifact.subType : 'none',
faceInfo: _faceInfo,
faceId: faceId,
},
})

View File

@ -400,7 +400,7 @@ const sketch001 = startSketchOn(part001, seg01)`)
ast,
sketchPathToNode,
extrudePathToNode,
'end'
{ type: 'cap', subType: 'end' }
)
if (err(extruded)) throw extruded
const { modifiedAst } = extruded

View File

@ -41,6 +41,7 @@ import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { SimplifiedArgDetails } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine'
export function startSketchOnDefault(
node: Program,
@ -442,7 +443,7 @@ export function sketchOnExtrudedFace(
node: Program,
sketchPathToNode: PathToNode,
extrudePathToNode: PathToNode,
cap: 'none' | 'start' | 'end' = 'none'
info: ExtrudeFacePlane['faceInfo'] = { type: 'wall' }
): { modifiedAst: Program; pathToNode: PathToNode } | Error {
let _node = { ...node }
const newSketchName = findUniqueName(
@ -476,21 +477,22 @@ export function sketchOnExtrudedFace(
const { node: extrudeVarDec } = _node3
const extrudeName = extrudeVarDec.id?.name
let _tag = null
if (cap === 'none') {
let _tag
if (info.type !== 'cap') {
const __tag = addTagForSketchOnFace(
{
pathToNode: sketchPathToNode,
node: _node,
},
expression.callee.name
expression.callee.name,
info.type === 'edgeCut' ? info : null
)
if (err(__tag)) return __tag
const { modifiedAst, tag } = __tag
_tag = createIdentifier(tag)
_node = modifiedAst
} else {
_tag = createLiteral(cap.toUpperCase())
_tag = createLiteral(info.subType.toUpperCase())
}
const newSketch = createVariableDeclaration(

View File

@ -234,7 +234,8 @@ function mutateAstWithTagForSketchSegment(
pathToNode: pathToSegmentNode,
node: astClone,
},
segmentNode.node.callee.name
segmentNode.node.callee.name,
null
)
if (err(taggedSegment)) return taggedSegment
const { tag } = taggedSegment

View File

@ -42,7 +42,7 @@ export interface PathArtifactRich {
codeRef: CommonCommandProperties
}
interface SegmentArtifact {
export interface SegmentArtifact {
type: 'segment'
pathId: ArtifactId
surfaceId: ArtifactId
@ -436,10 +436,10 @@ export function getArtifactsToUpdate({
response.data.modeling_response.type === 'solid3d_get_opposite_edge' &&
response.data.modeling_response.data.edge) ||
// or is adjacent edge
(cmd.type === 'solid3d_get_prev_adjacent_edge' &&
(cmd.type === 'solid3d_get_next_adjacent_edge' &&
response.type === 'modeling' &&
response.data.modeling_response.type ===
'solid3d_get_prev_adjacent_edge' &&
'solid3d_get_next_adjacent_edge' &&
response.data.modeling_response.data.edge)
) {
const wall = getArtifact(cmd.face_id)
@ -457,7 +457,7 @@ export function getArtifactsToUpdate({
artifact: {
type: 'sweepEdge',
subType:
cmd.type === 'solid3d_get_prev_adjacent_edge'
cmd.type === 'solid3d_get_next_adjacent_edge'
? 'adjacent'
: 'opposite',
segId: cmd.edge_id,
@ -724,20 +724,54 @@ export function getSweepEdgeCodeRef(
if (err(seg)) return seg
return seg.codeRef
}
export function getEdgeCuteConsumedCodeRef(
edge: EdgeCut,
artifactGraph: ArtifactGraph
): CommonCommandProperties | Error {
const seg = getArtifactOfTypes(
{ key: edge.consumedEdgeId, types: ['segment', 'sweepEdge'] },
artifactGraph
)
if (err(seg)) return seg
if (seg.type === 'segment') return seg.codeRef
return getSweepEdgeCodeRef(seg, artifactGraph)
}
export function getSweepFromSuspectedSweepSurface(
id: ArtifactId,
artifactGraph: ArtifactGraph
): SweepArtifact | Error {
const artifact = getArtifactOfTypes(
{ key: id, types: ['wall', 'cap'] },
{ key: id, types: ['wall', 'cap', 'edgeCut'] },
artifactGraph
)
if (err(artifact)) return artifact
if (artifact.type === 'wall' || artifact.type === 'cap') {
return getArtifactOfTypes(
{ key: artifact.sweepId, types: ['sweep'] },
artifactGraph
)
}
const segOrEdge = getArtifactOfTypes(
{ key: artifact.consumedEdgeId, types: ['segment', 'sweepEdge'] },
artifactGraph
)
if (err(segOrEdge)) return segOrEdge
if (segOrEdge.type === 'segment') {
const path = getArtifactOfTypes(
{ key: segOrEdge.pathId, types: ['path'] },
artifactGraph
)
if (err(path)) return path
return getArtifactOfTypes(
{ key: path.sweepId, types: ['sweep'] },
artifactGraph
)
}
return getArtifactOfTypes(
{ key: segOrEdge.sweepId, types: ['sweep'] },
artifactGraph
)
}
export function getSweepFromSuspectedPath(

View File

@ -231,7 +231,8 @@ describe('testing addTagForSketchOnFace', () => {
pathToNode,
node: ast,
},
'lineTo'
'lineTo',
null
)
if (err(sketchOnFaceRetVal)) return sketchOnFaceRetVal
@ -239,6 +240,62 @@ describe('testing addTagForSketchOnFace', () => {
const expectedCode = genCode('lineTo([-1.59, -1.54], %, $seg01)')
expect(recast(modifiedAst)).toBe(expectedCode)
})
it('can break up chamfers in order to add tags', async () => {
const originalChamfer = `|> chamfer({
length: 30,
tags: [seg01, getOppositeEdge(seg01)]
}, %)`
const genCode = (
insertCode: string
) => `const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
${insertCode}
`
const code = genCode(originalChamfer)
const ast = parse(code)
await enginelessExecutor(ast)
const sourceStart = code.indexOf(originalChamfer)
const sourceRange: [number, number] = [
sourceStart + 3,
sourceStart + originalChamfer.length - 3,
]
if (err(ast)) throw ast
const pathToNode = getNodePathFromSourceRange(ast, sourceRange)
console.log('pathToNode', pathToNode)
const sketchOnFaceRetVal = addTagForSketchOnFace(
{
pathToNode,
node: ast,
},
'chamfer',
{
type: 'edgeCut',
subType: 'opposite',
tagName: 'seg01',
}
)
if (err(sketchOnFaceRetVal)) throw sketchOnFaceRetVal
expect(recast(sketchOnFaceRetVal.modifiedAst)).toBe(
genCode(`|> chamfer({
length: 30,
tags: [getOppositeEdge(seg01)]
}, %, $seg03)
|> chamfer({ length: 30, tags: [seg01] }, %)`)
)
})
})
describe('testing getConstraintInfo', () => {

View File

@ -53,6 +53,7 @@ import { roundOff, getLength, getAngle } from 'lib/utils'
import { err } from 'lib/trap'
import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { EdgeCutInfo } from 'machines/modelingMachine'
const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"'
@ -1895,13 +1896,131 @@ export function replaceSketchLine({
return { modifiedAst, valueUsedInTransform, pathToNode }
}
/** Ostensibly should be used to add a chamfer tag to a chamfer call expression
*
* However things get complicated in situations like:
* ```ts
* |> chamfer({
* length: 1,
* tags: [tag1, tagOfInterest]
* }, %)
* ```
* Because tag declarator is not allowed on a chamfer with more than one tag,
* They must be pulled apart into separate chamfer calls:
* ```ts
* |> chamfer({
* length: 1,
* tags: [tag1]
* }, %)
* |> chamfer({
* length: 1,
* tags: [tagOfInterest]
* }, %, $newTagDeclarator)
* ```
*/
function addTagToChamfer(
tagInfo: AddTagInfo,
edgeCutMeta: EdgeCutInfo | null
):
| {
modifiedAst: Program
tag: string
}
| Error {
const _node = structuredClone(tagInfo.node)
let pipeIndex = 0
for (let i = 0; i < tagInfo.pathToNode.length; i++) {
if (tagInfo.pathToNode[i][1] === 'PipeExpression') {
pipeIndex = Number(tagInfo.pathToNode[i + 1][0])
break
}
}
const pipeExpr = getNodeFromPath<PipeExpression>(
_node,
tagInfo.pathToNode,
'PipeExpression'
)
if (err(pipeExpr)) return pipeExpr
const callExpr = pipeExpr.node.body[pipeIndex]
if (callExpr.type !== 'CallExpression')
return new Error('no chamfer call Expr')
const obj = callExpr.arguments[0]
if (obj.type !== 'ObjectExpression')
return new Error('first argument should be an object expression')
const tags = obj.properties.find((a) => {
return a.key.name === 'tags'
})
if (!tags) return new Error('no tags property')
if (tags.value.type !== 'ArrayExpression')
return new Error('tags should be an array expression')
if (tags.value.elements.length < 2) {
return addTag(2)(tagInfo)
}
const tagIndexToPullOut = tags.value.elements.findIndex((element) => {
if (
edgeCutMeta?.subType === 'base' &&
element.type === 'Identifier' &&
element.name === edgeCutMeta.tagName
)
return true
if (
edgeCutMeta?.subType === 'opposite' &&
element.type === 'CallExpression' &&
element.callee.name === 'getOppositeEdge' &&
element.arguments[0].type === 'Identifier' &&
element.arguments[0].name === edgeCutMeta.tagName
)
return true
if (
edgeCutMeta?.subType === 'adjacent' &&
element.type === 'CallExpression' &&
(element.callee.name === 'getNextAdjacentEdge' ||
element.callee.name === 'getPrevAdjacentEdge') &&
element.arguments[0].type === 'Identifier' &&
element.arguments[0].name === edgeCutMeta.tagName
)
return true
return false
})
if (tagIndexToPullOut === -1) return new Error('tag not found')
const tagToPullOut = tags.value.elements[tagIndexToPullOut]
tags.value.elements.splice(tagIndexToPullOut, 1)
const chamferLength = obj.properties.find(
(a) => a.key.name === 'length'
)?.value
if (!chamferLength) return new Error('no chamfer length')
const tagDec = createTagDeclarator(findUniqueName(_node, 'seg', 2))
const newExpressionToInsert = createCallExpression('chamfer', [
createObjectExpression({
length: chamferLength,
tags: createArrayExpression([tagToPullOut]),
}),
createPipeSubstitution(),
tagDec,
])
pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert)
return {
modifiedAst: _node,
tag: tagDec.value,
}
}
export function addTagForSketchOnFace(
tagInfo: AddTagInfo,
expressionName: string
) {
expressionName: string,
edgeCutMeta: EdgeCutInfo | null
):
| {
modifiedAst: Program
tag: string
}
| Error {
if (expressionName === 'close') {
return addTag(1)(tagInfo)
}
if (expressionName === 'chamfer') {
return addTagToChamfer(tagInfo, edgeCutMeta)
}
if (expressionName in sketchLineHelperMap) {
const { addTag } = sketchLineHelperMap[expressionName]
return addTag(tagInfo)

View File

@ -18,12 +18,24 @@ export function pathMapToSelections(
const nodeMeta = getNodeFromPath<any>(ast, path)
if (err(nodeMeta)) return
const node = nodeMeta.node as any
const type = prevSelections.codeBasedSelections[Number(index)].type
const selection = prevSelections.codeBasedSelections[Number(index)]
if (node) {
if (
selection.type === 'base-edgeCut' ||
selection.type === 'adjacent-edgeCut' ||
selection.type === 'opposite-edgeCut'
) {
newSelections.codeBasedSelections.push({
range: [node.start, node.end],
type: type || 'default',
type: selection.type,
secondaryRange: selection.secondaryRange,
})
} else {
newSelections.codeBasedSelections.push({
range: [node.start, node.end],
type: selection.type,
})
}
}
})
return newSelections

View File

@ -41,7 +41,8 @@ export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
export type Selection = {
export type Selection =
| {
type:
| 'default'
| 'line-end'
@ -57,7 +58,13 @@ export type Selection = {
| 'arc'
| 'all'
range: SourceRange
}
}
| {
type: 'opposite-edgeCut' | 'adjacent-edgeCut' | 'base-edgeCut'
range: SourceRange
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
secondaryRange: SourceRange
}
export type Selections = {
otherSelections: Axis[]
codeBasedSelections: Selection[]
@ -164,6 +171,52 @@ export async function getEventForSelectWithPoint({
},
}
}
if (_artifact.type === 'edgeCut') {
const consumedEdge = getArtifactOfTypes(
{ key: _artifact.consumedEdgeId, types: ['segment', 'sweepEdge'] },
engineCommandManager.artifactGraph
)
if (err(consumedEdge))
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: _artifact.codeRef.range, type: 'default' },
},
}
if (consumedEdge.type === 'segment') {
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: _artifact.codeRef.range,
type: 'base-edgeCut',
secondaryRange: consumedEdge.codeRef.range,
},
},
}
}
const segment = getArtifactOfTypes(
{ key: consumedEdge.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(segment)) return null
return {
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: {
range: _artifact.codeRef.range,
type:
consumedEdge.subType === 'adjacent'
? 'adjacent-edgeCut'
: 'opposite-edgeCut',
secondaryRange: segment.codeRef.range,
},
},
}
}
return null
}
@ -625,6 +678,54 @@ function codeToIdSelections(
}
return
}
if (entry.artifact.type === 'edgeCut') {
const consumedEdge = getArtifactOfTypes(
{
key: entry.artifact.consumedEdgeId,
types: ['segment', 'sweepEdge'],
},
engineCommandManager.artifactGraph
)
if (err(consumedEdge)) return
if (
consumedEdge.type === 'segment' &&
type === 'base-edgeCut' &&
isOverlap(
consumedEdge.codeRef.range,
entry.selection?.secondaryRange || [0, 0]
)
) {
bestCandidate = {
artifact: entry.artifact,
selection: { type, range, ...rest },
id: entry.id,
}
} else if (
consumedEdge.type === 'sweepEdge' &&
((type === 'adjacent-edgeCut' &&
consumedEdge.subType === 'adjacent') ||
(type === 'opposite-edgeCut' &&
consumedEdge.subType === 'opposite'))
) {
const seg = getArtifactOfTypes(
{ key: consumedEdge.segId, types: ['segment'] },
engineCommandManager.artifactGraph
)
if (err(seg)) return
if (
isOverlap(
seg.codeRef.range,
entry.selection?.secondaryRange || [0, 0]
)
) {
bestCandidate = {
artifact: entry.artifact,
selection: { type, range, ...rest },
id: entry.id,
}
}
}
}
})
if (bestCandidate) {
@ -686,9 +787,20 @@ export function updateSelections(
const nodeMeta = getNodeFromPath<Expr>(ast, pathToNode)
if (err(nodeMeta)) return undefined
const node = nodeMeta.node
const selection = prevSelectionRanges.codeBasedSelections[Number(index)]
if (
selection?.type === 'base-edgeCut' ||
selection?.type === 'adjacent-edgeCut' ||
selection?.type === 'opposite-edgeCut'
)
return {
range: [node.start, node.end],
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
type: selection?.type,
secondaryRange: selection?.secondaryRange,
}
return {
range: [node.start, node.end],
type: selection?.type,
}
})
.filter((x?: Selection) => x !== undefined) as Selection[]

File diff suppressed because one or more lines are too long

View File

@ -216,7 +216,7 @@ pub(crate) async fn do_post_extrude(
args.batch_modeling_cmd(
uuid::Uuid::new_v4(),
ModelingCmd::from(mcmd::Solid3dGetPrevAdjacentEdge {
ModelingCmd::from(mcmd::Solid3dGetNextAdjacentEdge {
edge_id: curve_id,
object_id: sketch_group.id,
face_id,

View File

@ -0,0 +1,23 @@
const sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
217.26
], %, $seg01)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $yo)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|> close(%)
const extrude001 = extrude(100, sketch001)
|> chamfer({
length: 30,
tags: [
seg01,
getNextAdjacentEdge(yo),
getNextAdjacentEdge(seg02),
getOppositeEdge(seg01)
]
}, %)

View File

@ -0,0 +1,51 @@
const sketch001 = startSketchOn('XZ')
|> startProfileAt([23.53, 152.5], %)
|> angledLine([0, 172.25], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
110.07
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude001 = extrude(100, sketch001)
const sketch004 = startSketchOn(extrude001, 'END')
|> startProfileAt([81.71, 89.86], %)
|> line([26.96, 39.72], %)
|> line([37.27, -41.68], %, $seg03)
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg04)
|> close(%)
const extrude003 = extrude(100, sketch004)
|> chamfer({
length: 22,
tags: [getNextAdjacentEdge(seg04), getOppositeEdge(seg03)]
}, %)
const sketch002 = startSketchOn('XZ')
|> startProfileAt([272.24, 46.63], %)
|> angledLine([0, 150.77], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) + 90,
215.94
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude002 = extrude(160, sketch002)
const sketch003 = startSketchOn(extrude002, rectangleSegmentC002)
|> startProfileAt([-340.96, 50.64], %)
|> line([29.12, 77.97], %, $seg02)
|> line([19.74, -70.08], %, $seg01)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
const extrude004 = extrude(100, sketch003)
|> chamfer({
length: 18,
tags: [getNextAdjacentEdge(seg01), seg02]
}, %)