Sketch on chamfer UI (#3918)
* sketch on chamfer start * working * step app from getting in weird state when selection face to sketch on * sketch on chamfer tests * clean up * fix test * fix click selections for chamfers, add tests * fixture setup (#3964) * initial break up * rename main fixture file * add more expect state pattern * add fixture comment * add comments to chamfer function * typos * works without pipeExpr
This commit is contained in:
@ -1,216 +0,0 @@
|
||||
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'
|
||||
|
||||
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 codeContent: Locator
|
||||
private readonly extrudeButton: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.codeContent = page.locator('.cm-content')
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
}
|
||||
|
||||
async initialise(code = '') {
|
||||
const u = await getUtils(this.page)
|
||||
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) => [
|
||||
() => this.page.mouse.click(x, y),
|
||||
() => this.page.mouse.move(x, y),
|
||||
]
|
||||
|
||||
/** 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) =>
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const texts = (
|
||||
await this.page.getByTestId('hover-highlight').allInnerTexts()
|
||||
).map((s) => s.replace(/\s+/g, '').trim())
|
||||
return texts.join('')
|
||||
})
|
||||
.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(() => this.codeContent.textContent(), {
|
||||
timeout,
|
||||
})
|
||||
if (not) {
|
||||
return expectStart.not.toContain(normalisedCode)
|
||||
}
|
||||
return expectStart.toContain(normalisedCode)
|
||||
}
|
||||
|
||||
expectEditor = {
|
||||
toContain: this._expectEditorToContain(),
|
||||
not: { toContain: this._expectEditorToContain(true) },
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
116
e2e/playwright/cmdBarFixture.ts
Normal file
116
e2e/playwright/cmdBarFixture.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
type CmdBarSerialised =
|
||||
| {
|
||||
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 CmdBarFixture {
|
||||
public readonly page: Page
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
|
||||
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 || '',
|
||||
}
|
||||
}
|
||||
expectState = async (expected: CmdBarSerialised) => {
|
||||
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
|
||||
}
|
||||
/** The method will use buttons OR press enter randomly to progress the cmdbar,
|
||||
* this could have unexpected results depending on what's focused
|
||||
*
|
||||
* TODO: This method assumes the user has a valid input to the current stage,
|
||||
* and assumes we are past the `pickCommand` step.
|
||||
*/
|
||||
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
|
||||
if (shouldFuzzProgressMethod || 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')
|
||||
}
|
||||
}
|
||||
}
|
109
e2e/playwright/editorFixture.ts
Normal file
109
e2e/playwright/editorFixture.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
interface EditorState {
|
||||
activeLines: Array<string>
|
||||
highlightedCode: string
|
||||
diagnostics: Array<string>
|
||||
}
|
||||
|
||||
function removeWhitespace(str: string) {
|
||||
return str.replace(/\s+/g, '').trim()
|
||||
}
|
||||
export class EditorFixture {
|
||||
public readonly page: Page
|
||||
|
||||
private readonly diagnosticsTooltip: Locator
|
||||
private readonly diagnosticsGutterIcon: Locator
|
||||
private readonly codeContent: Locator
|
||||
private readonly activeLine: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
|
||||
this.codeContent = page.locator('.cm-content')
|
||||
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
|
||||
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')
|
||||
this.activeLine = this.page.locator('.cm-activeLine')
|
||||
}
|
||||
|
||||
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) },
|
||||
}
|
||||
private _serialiseDiagnostics = async (): Promise<Array<string>> => {
|
||||
const diagnostics = await this.diagnosticsGutterIcon.all()
|
||||
const diagnosticsContent: string[] = []
|
||||
for (const diag of diagnostics) {
|
||||
await diag.hover()
|
||||
const content = await this.diagnosticsTooltip.allTextContents()
|
||||
diagnosticsContent.push(content.join(''))
|
||||
}
|
||||
return [...new Set(diagnosticsContent)].map((d) => d.trim())
|
||||
}
|
||||
|
||||
private _getHighlightedCode = async () => {
|
||||
const texts = (
|
||||
await this.page.getByTestId('hover-highlight').allInnerTexts()
|
||||
).map((s) => s.replace(/\s+/g, '').trim())
|
||||
return texts.join('')
|
||||
}
|
||||
private _getActiveLines = async () =>
|
||||
(await this.activeLine.allInnerTexts()).map((l) => l.trim())
|
||||
expectActiveLinesToBe = async (lines: Array<string>) => {
|
||||
await expect.poll(this._getActiveLines).toEqual(lines.map((l) => l.trim()))
|
||||
}
|
||||
/** assert all editor state EXCEPT the code content */
|
||||
expectState = async (expectedState: EditorState) => {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [activeLines, highlightedCode, diagnostics] = await Promise.all([
|
||||
this._getActiveLines(),
|
||||
this._getHighlightedCode(),
|
||||
this._serialiseDiagnostics(),
|
||||
])
|
||||
const state: EditorState = {
|
||||
activeLines: activeLines.map(removeWhitespace).filter(Boolean),
|
||||
highlightedCode: removeWhitespace(highlightedCode),
|
||||
diagnostics,
|
||||
}
|
||||
return state
|
||||
})
|
||||
.toEqual({
|
||||
activeLines: expectedState.activeLines.map(removeWhitespace),
|
||||
highlightedCode: removeWhitespace(expectedState.highlightedCode),
|
||||
diagnostics: expectedState.diagnostics.map(removeWhitespace),
|
||||
})
|
||||
}
|
||||
}
|
70
e2e/playwright/fixtureSetup.ts
Normal file
70
e2e/playwright/fixtureSetup.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
import fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { CmdBarFixture } from './cmdBarFixture'
|
||||
import { EditorFixture } from './editorFixture'
|
||||
import { ToolbarFixture } from './toolbarFixture'
|
||||
import { SceneFixture } from './sceneFixture'
|
||||
|
||||
export class AuthenticatedApp {
|
||||
public readonly page: Page
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
async initialise(code = '') {
|
||||
const u = await getUtils(this.page)
|
||||
|
||||
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'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const test = base.extend<{
|
||||
app: AuthenticatedApp
|
||||
cmdBar: CmdBarFixture
|
||||
editor: EditorFixture
|
||||
toolbar: ToolbarFixture
|
||||
scene: SceneFixture
|
||||
}>({
|
||||
app: async ({ page }, use) => {
|
||||
await use(new AuthenticatedApp(page))
|
||||
},
|
||||
cmdBar: async ({ page }, use) => {
|
||||
await use(new CmdBarFixture(page))
|
||||
},
|
||||
editor: async ({ page }, use) => {
|
||||
await use(new EditorFixture(page))
|
||||
},
|
||||
toolbar: async ({ page }, use) => {
|
||||
await use(new ToolbarFixture(page))
|
||||
},
|
||||
scene: async ({ page }, use) => {
|
||||
await use(new SceneFixture(page))
|
||||
},
|
||||
})
|
||||
|
||||
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'
|
@ -1,36 +1,53 @@
|
||||
import { test } from './authenticatedAppFixture'
|
||||
import { test, expect, AuthenticatedApp } from './fixtureSetup'
|
||||
import { EditorFixture } from './editorFixture'
|
||||
import { SceneFixture } from './sceneFixture'
|
||||
import { ToolbarFixture } from './toolbarFixture'
|
||||
|
||||
// test file is for testing point an click code gen functionality that's not sketch mode related
|
||||
|
||||
test('verify extruding circle works', async ({ app }) => {
|
||||
test('verify extruding circle works', async ({
|
||||
app,
|
||||
cmdBar,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'Fails on windows in CI, can not be replicated locally on windows.'
|
||||
)
|
||||
const file = await app.getInputFile('test-circle-extrude.kcl')
|
||||
await app.initialise(file)
|
||||
const [clickCircle, moveToCircle] = app.makeMouseHelpers(582, 217)
|
||||
const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217)
|
||||
|
||||
await test.step('because there is sweepable geometry, verify extrude is enable when nothing is selected', async () => {
|
||||
await app.clickNoWhere()
|
||||
await app.expectExtrudeButtonToBeEnabled()
|
||||
await scene.clickNoWhere()
|
||||
await expect(toolbar.extrudeButton).toBeEnabled()
|
||||
})
|
||||
|
||||
await test.step('check code model connection works and that button is still enable once circle is selected ', async () => {
|
||||
await moveToCircle()
|
||||
const circleSnippet =
|
||||
'circle({ center: [318.33, 168.1], radius: 182.8 }, %)'
|
||||
await app.expectCodeHighlightedToBe(circleSnippet)
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
highlightedCode: circleSnippet,
|
||||
diagnostics: [],
|
||||
})
|
||||
|
||||
await clickCircle()
|
||||
await app.expectActiveLinesToBe([circleSnippet.slice(-5)])
|
||||
await app.expectExtrudeButtonToBeEnabled()
|
||||
await editor.expectState({
|
||||
activeLines: [circleSnippet.slice(-5)],
|
||||
highlightedCode: circleSnippet,
|
||||
diagnostics: [],
|
||||
})
|
||||
await expect(toolbar.extrudeButton).toBeEnabled()
|
||||
})
|
||||
|
||||
await test.step('do extrude flow and check extrude code is added to editor', async () => {
|
||||
await app.clickExtrudeButton()
|
||||
await toolbar.extrudeButton.click()
|
||||
|
||||
await app.expectCmdBarToBe({
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'distance',
|
||||
currentArgValue: '5',
|
||||
@ -38,18 +55,399 @@ test('verify extruding circle works', async ({ app }) => {
|
||||
highlightedHeaderArg: 'distance',
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
await app.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
const expectString = 'const extrude001 = extrude(5, sketch001)'
|
||||
await app.expectEditor.not.toContain(expectString)
|
||||
await editor.expectEditor.not.toContain(expectString)
|
||||
|
||||
await app.expectCmdBarToBe({
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Selection: '1 face', Distance: '5' },
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
await app.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
await app.expectEditor.toContain(expectString)
|
||||
await editor.expectEditor.toContain(expectString)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('verify sketch on chamfer works', () => {
|
||||
const _sketchOnAChamfer =
|
||||
(
|
||||
app: AuthenticatedApp,
|
||||
editor: EditorFixture,
|
||||
toolbar: ToolbarFixture,
|
||||
scene: SceneFixture
|
||||
) =>
|
||||
async ({
|
||||
clickCoords,
|
||||
cameraPos,
|
||||
cameraTarget,
|
||||
beforeChamferSnippet,
|
||||
afterChamferSelectSnippet,
|
||||
afterRectangle1stClickSnippet,
|
||||
afterRectangle2ndClickSnippet,
|
||||
beforeChamferSnippetEnd,
|
||||
}: {
|
||||
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
|
||||
beforeChamferSnippetEnd?: string
|
||||
}) => {
|
||||
const [clickChamfer] = scene.makeMouseHelpers(
|
||||
clickCoords.x,
|
||||
clickCoords.y
|
||||
)
|
||||
const [rectangle1stClick] = scene.makeMouseHelpers(573, 149)
|
||||
const [rectangle2ndClick, rectangle2ndMove] = scene.makeMouseHelpers(
|
||||
598,
|
||||
380,
|
||||
{ steps: 5 }
|
||||
)
|
||||
|
||||
await scene.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 editor.expectActiveLinesToBe([beforeChamferSnippet.slice(-5)])
|
||||
await editor.expectActiveLinesToBe([
|
||||
beforeChamferSnippetEnd || beforeChamferSnippet.slice(-5),
|
||||
])
|
||||
}).toPass({ timeout: 15_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 toolbar.startSketchPlaneSelection()
|
||||
await clickChamfer()
|
||||
// timeout wait for engine animation is unavoidable
|
||||
await app.page.waitForTimeout(600)
|
||||
await editor.expectEditor.toContain(afterChamferSelectSnippet)
|
||||
})
|
||||
await test.step('make sure a basic sketch can be added', async () => {
|
||||
await toolbar.rectangleBtn.click()
|
||||
await rectangle1stClick()
|
||||
await editor.expectEditor.toContain(afterRectangle1stClickSnippet)
|
||||
await rectangle2ndMove({
|
||||
pixelDiff: 50,
|
||||
})
|
||||
await rectangle2ndClick()
|
||||
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet)
|
||||
})
|
||||
|
||||
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
|
||||
await toolbar.exitSketchBtn.click()
|
||||
await scene.waitForExecutionDone()
|
||||
})
|
||||
await test.step('Check there is no errors after code created in previous steps executes', async () => {
|
||||
await editor.expectState({
|
||||
activeLines: ["const sketch001 = startSketchOn('XZ')"],
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
}
|
||||
test('works on all edge selections and can break up multi edges in a chamfer array', async ({
|
||||
app,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
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, editor, toolbar, scene)
|
||||
|
||||
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('verify at the end of the test that final code is what is expected', async () => {
|
||||
await editor.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 that are non in a pipeExpression can break up multi edges in a chamfer array', async ({
|
||||
app,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
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-no-pipeExpr.kcl'
|
||||
)
|
||||
await app.initialise(file)
|
||||
|
||||
const sketchOnAChamfer = _sketchOnAChamfer(app, editor, toolbar, scene)
|
||||
|
||||
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)
|
||||
]}, extrude001)`,
|
||||
beforeChamferSnippetEnd: '}, extrude001)',
|
||||
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 editor.expectEditor.toContain(
|
||||
`const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([75.8, 317.2], %)
|
||||
|> 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)
|
||||
const chamf = chamfer({
|
||||
length: 30,
|
||||
tags: [getOppositeEdge(seg01)]
|
||||
}, extrude001, $seg03)
|
||||
|> chamfer({
|
||||
length: 30,
|
||||
tags: [
|
||||
seg01,
|
||||
getNextAdjacentEdge(yo),
|
||||
getNextAdjacentEdge(seg02)
|
||||
]
|
||||
}, %)
|
||||
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 }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
81
e2e/playwright/sceneFixture.ts
Normal file
81
e2e/playwright/sceneFixture.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import {
|
||||
closeDebugPanel,
|
||||
doAndWaitForImageDiff,
|
||||
openAndClearDebugPanel,
|
||||
sendCustomCmd,
|
||||
} from './test-utils'
|
||||
|
||||
type mouseParams = {
|
||||
pixelDiff: number
|
||||
}
|
||||
|
||||
export class SceneFixture {
|
||||
public readonly page: Page
|
||||
private readonly exeIndicator: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
}
|
||||
|
||||
makeMouseHelpers = (
|
||||
x: number,
|
||||
y: number,
|
||||
{ steps }: { steps: number } = { steps: 5000 }
|
||||
) => [
|
||||
(params?: mouseParams) => {
|
||||
if (params?.pixelDiff) {
|
||||
return doAndWaitForImageDiff(
|
||||
this.page,
|
||||
() => this.page.mouse.click(x, y),
|
||||
params.pixelDiff
|
||||
)
|
||||
}
|
||||
return this.page.mouse.click(x, y)
|
||||
},
|
||||
(params?: mouseParams) => {
|
||||
if (params?.pixelDiff) {
|
||||
return doAndWaitForImageDiff(
|
||||
this.page,
|
||||
() => this.page.mouse.move(x, y, { steps }),
|
||||
params.pixelDiff
|
||||
)
|
||||
}
|
||||
return 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)
|
||||
|
||||
moveCameraTo = async (
|
||||
pos: { x: number; y: number; z: number },
|
||||
target: { x: number; y: number; z: number } = { x: 0, y: 0, z: 0 }
|
||||
) => {
|
||||
await openAndClearDebugPanel(this.page)
|
||||
await doAndWaitForImageDiff(
|
||||
this.page,
|
||||
() =>
|
||||
sendCustomCmd(this.page, {
|
||||
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 closeDebugPanel(this.page)
|
||||
}
|
||||
waitForExecutionDone = async () => {
|
||||
await expect(this.exeIndicator).toBeVisible()
|
||||
}
|
||||
}
|
@ -87,7 +87,7 @@ async function removeCurrentCode(page: Page) {
|
||||
await expect(page.locator('.cm-content')).toHaveText('')
|
||||
}
|
||||
|
||||
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
||||
export async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
||||
await page.getByTestId('custom-cmd-input').fill(JSON.stringify(cmd))
|
||||
await page.getByTestId('custom-cmd-send-button').click()
|
||||
}
|
||||
@ -140,7 +140,7 @@ async function openDebugPanel(page: Page) {
|
||||
await openPane(page, 'debug-pane-button')
|
||||
}
|
||||
|
||||
async function closeDebugPanel(page: Page) {
|
||||
export async function closeDebugPanel(page: Page) {
|
||||
const debugLocator = page.getByTestId('debug-pane-button')
|
||||
await expect(debugLocator).toBeVisible()
|
||||
const isOpen = (await debugLocator?.getAttribute('aria-pressed')) === 'true'
|
||||
@ -355,10 +355,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
closeFilePanel: () => closeFilePanel(page),
|
||||
openVariablesPane: () => openVariablesPane(page),
|
||||
openLogsPane: () => openLogsPane(page),
|
||||
openAndClearDebugPanel: async () => {
|
||||
await openDebugPanel(page)
|
||||
return clearCommandLogs(page)
|
||||
},
|
||||
openAndClearDebugPanel: () => openAndClearDebugPanel(page),
|
||||
clearAndCloseDebugPanel: async () => {
|
||||
await clearCommandLogs(page)
|
||||
return closeDebugPanel(page)
|
||||
@ -470,50 +467,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
})
|
||||
},
|
||||
doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
;(async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
;(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
}
|
||||
})().catch(reportRejection)
|
||||
}, 50)
|
||||
})().catch(reportRejection)
|
||||
}),
|
||||
doAndWaitForImageDiff(page, fn, diffCount),
|
||||
emulateNetworkConditions: async (
|
||||
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
||||
) => {
|
||||
@ -1056,3 +1010,59 @@ export async function createProjectAndRenameIt({
|
||||
export function executorInputPath(fileName: string): string {
|
||||
return join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName)
|
||||
}
|
||||
|
||||
export async function doAndWaitForImageDiff(
|
||||
page: Page,
|
||||
fn: () => Promise<unknown>,
|
||||
diffCount = 200
|
||||
) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
;(async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp1.png',
|
||||
fullPage: true,
|
||||
})
|
||||
await fn()
|
||||
const isImageDiff = async () => {
|
||||
await page.screenshot({
|
||||
path: './e2e/playwright/temp2.png',
|
||||
fullPage: true,
|
||||
})
|
||||
const screenshot1 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp1.png')
|
||||
)
|
||||
const screenshot2 = PNG.sync.read(
|
||||
await fsp.readFile('./e2e/playwright/temp2.png')
|
||||
)
|
||||
const actualDiffCount = pixelMatch(
|
||||
screenshot1.data,
|
||||
screenshot2.data,
|
||||
null,
|
||||
screenshot1.width,
|
||||
screenshot2.height
|
||||
)
|
||||
return actualDiffCount > diffCount
|
||||
}
|
||||
|
||||
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
||||
let count = 0
|
||||
const interval = setInterval(() => {
|
||||
;(async () => {
|
||||
count++
|
||||
if (await isImageDiff()) {
|
||||
clearInterval(interval)
|
||||
resolve(true)
|
||||
} else if (count > 100) {
|
||||
clearInterval(interval)
|
||||
resolve(false)
|
||||
}
|
||||
})().catch(reportRejection)
|
||||
}, 50)
|
||||
})().catch(reportRejection)
|
||||
})
|
||||
}
|
||||
|
||||
export async function openAndClearDebugPanel(page: Page) {
|
||||
await openDebugPanel(page)
|
||||
return clearCommandLogs(page)
|
||||
}
|
||||
|
@ -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 engine', 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 ({
|
||||
|
21
e2e/playwright/toolbarFixture.ts
Normal file
21
e2e/playwright/toolbarFixture.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { doAndWaitForImageDiff } from './test-utils'
|
||||
|
||||
export class ToolbarFixture {
|
||||
public readonly page: Page
|
||||
readonly extrudeButton: Locator
|
||||
readonly startSketchBtn: Locator
|
||||
readonly rectangleBtn: Locator
|
||||
readonly exitSketchBtn: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
||||
this.exitSketchBtn = page.getByTestId('sketch-exit')
|
||||
}
|
||||
|
||||
startSketchPlaneSelection = async () =>
|
||||
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
|
||||
}
|
@ -579,9 +579,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)
|
||||
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -4,19 +4,20 @@ import {
|
||||
ArrayExpression,
|
||||
BinaryExpression,
|
||||
CallExpression,
|
||||
Expr,
|
||||
ExpressionStatement,
|
||||
ObjectExpression,
|
||||
ObjectProperty,
|
||||
PathToNode,
|
||||
PipeExpression,
|
||||
Program,
|
||||
ProgramMemory,
|
||||
ReturnStatement,
|
||||
sketchGroupFromKclValue,
|
||||
SourceRange,
|
||||
SyntaxType,
|
||||
Expr,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
sketchGroupFromKclValue,
|
||||
ObjectExpression,
|
||||
} from './wasm'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||
@ -947,7 +948,7 @@ export function doesSceneHaveSweepableSketch(ast: Program) {
|
||||
export function getObjExprProperty(
|
||||
node: ObjectExpression,
|
||||
propName: string
|
||||
): { expr: Expr; index: number } | null {
|
||||
): { expr: ObjectProperty['value']; index: number } | null {
|
||||
const index = node.properties.findIndex(({ key }) => key.name === propName)
|
||||
if (index === -1) return null
|
||||
return { expr: node.properties[index].value, index }
|
||||
|
@ -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(
|
||||
|
@ -234,7 +234,8 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
pathToNode,
|
||||
node: ast,
|
||||
},
|
||||
'lineTo'
|
||||
'lineTo',
|
||||
null
|
||||
)
|
||||
if (err(sketchOnFaceRetVal)) return sketchOnFaceRetVal
|
||||
|
||||
@ -242,6 +243,85 @@ describe('testing addTagForSketchOnFace', () => {
|
||||
const expectedCode = genCode('lineTo([-1.59, -1.54], %, $seg01)')
|
||||
expect(recast(modifiedAst)).toBe(expectedCode)
|
||||
})
|
||||
const chamferTestCases = [
|
||||
{
|
||||
desc: 'chamfer in pipeExpr',
|
||||
originalChamfer: ` |> chamfer({
|
||||
length: 30,
|
||||
tags: [seg01, getOppositeEdge(seg01)]
|
||||
}, %)`,
|
||||
expectedChamfer: ` |> chamfer({
|
||||
length: 30,
|
||||
tags: [getOppositeEdge(seg01)]
|
||||
}, %, $seg03)
|
||||
|> chamfer({ length: 30, tags: [seg01] }, %)`,
|
||||
},
|
||||
{
|
||||
desc: 'chamfer with its own variable',
|
||||
originalChamfer: `const chamf = chamfer({
|
||||
length: 30,
|
||||
tags: [seg01, getOppositeEdge(seg01)]
|
||||
}, extrude001)`,
|
||||
expectedChamfer: `const chamf = chamfer({
|
||||
length: 30,
|
||||
tags: [getOppositeEdge(seg01)]
|
||||
}, extrude001, $seg03)
|
||||
|> chamfer({ length: 30, tags: [seg01] }, %)`,
|
||||
},
|
||||
// Add more test cases here if needed
|
||||
] as const
|
||||
|
||||
chamferTestCases.forEach(({ originalChamfer, expectedChamfer, desc }) => {
|
||||
it.only(`can break up chamfers in order to add tags - ${desc}`, async () => {
|
||||
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 extraChars = originalChamfer.indexOf('chamfer')
|
||||
const sourceRange: [number, number] = [
|
||||
sourceStart + extraChars,
|
||||
sourceStart + originalChamfer.length - extraChars,
|
||||
]
|
||||
|
||||
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(expectedChamfer)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing getConstraintInfo', () => {
|
||||
|
@ -54,6 +54,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"'
|
||||
@ -2080,13 +2081,170 @@ 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'
|
||||
)
|
||||
const variableDec = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
tagInfo.pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(pipeExpr)) return pipeExpr
|
||||
if (err(variableDec)) return variableDec
|
||||
const isPipeExpression = pipeExpr.node.type === 'PipeExpression'
|
||||
|
||||
console.log('pipeExpr', pipeExpr, variableDec)
|
||||
// const callExpr = isPipeExpression ? pipeExpr.node.body[pipeIndex] : variableDec.node.init
|
||||
const callExpr = isPipeExpression
|
||||
? pipeExpr.node.body[pipeIndex]
|
||||
: variableDec.node.init
|
||||
if (callExpr.type !== 'CallExpression')
|
||||
return new Error('no chamfer call Expr')
|
||||
const chamferObjArg = callExpr.arguments[0]
|
||||
if (chamferObjArg.type !== 'ObjectExpression')
|
||||
return new Error('first argument should be an object expression')
|
||||
const inputTags = getObjExprProperty(chamferObjArg, 'tags')
|
||||
if (!inputTags) return new Error('no tags property')
|
||||
if (inputTags.expr.type !== 'ArrayExpression')
|
||||
return new Error('tags should be an array expression')
|
||||
|
||||
const isChamferBreakUpNeeded = inputTags.expr.elements.length > 1
|
||||
if (!isChamferBreakUpNeeded) {
|
||||
return addTag(2)(tagInfo)
|
||||
}
|
||||
|
||||
// There's more than one input tag, we need to break that chamfer call into a separate chamfer call
|
||||
// so that it can have a tag declarator added.
|
||||
const tagIndexToPullOut = inputTags.expr.elements.findIndex((tag) => {
|
||||
// e.g. chamfer({ tags: [tagOfInterest, tag2] }, %)
|
||||
// ^^^^^^^^^^^^^
|
||||
const elementMatchesBaseTagType =
|
||||
edgeCutMeta?.subType === 'base' &&
|
||||
tag.type === 'Identifier' &&
|
||||
tag.name === edgeCutMeta.tagName
|
||||
if (elementMatchesBaseTagType) return true
|
||||
|
||||
// e.g. chamfer({ tags: [getOppositeEdge(tagOfInterest), tag2] }, %)
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
const tagMatchesOppositeTagType =
|
||||
edgeCutMeta?.subType === 'opposite' &&
|
||||
tag.type === 'CallExpression' &&
|
||||
tag.callee.name === 'getOppositeEdge' &&
|
||||
tag.arguments[0].type === 'Identifier' &&
|
||||
tag.arguments[0].name === edgeCutMeta.tagName
|
||||
if (tagMatchesOppositeTagType) return true
|
||||
|
||||
// e.g. chamfer({ tags: [getNextAdjacentEdge(tagOfInterest), tag2] }, %)
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
const tagMatchesAdjacentTagType =
|
||||
edgeCutMeta?.subType === 'adjacent' &&
|
||||
tag.type === 'CallExpression' &&
|
||||
(tag.callee.name === 'getNextAdjacentEdge' ||
|
||||
tag.callee.name === 'getPrevAdjacentEdge') &&
|
||||
tag.arguments[0].type === 'Identifier' &&
|
||||
tag.arguments[0].name === edgeCutMeta.tagName
|
||||
if (tagMatchesAdjacentTagType) return true
|
||||
return false
|
||||
})
|
||||
if (tagIndexToPullOut === -1) return new Error('tag not found')
|
||||
// get the tag we're pulling out
|
||||
const tagToPullOut = inputTags.expr.elements[tagIndexToPullOut]
|
||||
// and remove it from the original chamfer call
|
||||
// [pullOutTag, tag2] to [tag2]
|
||||
inputTags.expr.elements.splice(tagIndexToPullOut, 1)
|
||||
|
||||
// get the length of the chamfer we're breaking up, as the new chamfer will have the same length
|
||||
const chamferLength = getObjExprProperty(chamferObjArg, 'length')
|
||||
if (!chamferLength) return new Error('no chamfer length')
|
||||
const tagDec = createTagDeclarator(findUniqueName(_node, 'seg', 2))
|
||||
const solid3dIdentifierUsedInOriginalChamfer = callExpr.arguments[1]
|
||||
const newExpressionToInsert = createCallExpression('chamfer', [
|
||||
createObjectExpression({
|
||||
length: chamferLength.expr,
|
||||
// single tag to add to the new chamfer call
|
||||
tags: createArrayExpression([tagToPullOut]),
|
||||
}),
|
||||
isPipeExpression
|
||||
? createPipeSubstitution()
|
||||
: solid3dIdentifierUsedInOriginalChamfer,
|
||||
tagDec,
|
||||
])
|
||||
|
||||
// insert the new chamfer call with the tag declarator, add its above the original
|
||||
// alternatively we could use `pipeIndex + 1` to insert it below the original
|
||||
if (isPipeExpression) {
|
||||
pipeExpr.node.body.splice(pipeIndex, 0, newExpressionToInsert)
|
||||
} else {
|
||||
console.log('yo', createPipeExpression([newExpressionToInsert, callExpr]))
|
||||
callExpr.arguments[1] = createPipeSubstitution()
|
||||
variableDec.node.init = createPipeExpression([
|
||||
newExpressionToInsert,
|
||||
callExpr,
|
||||
])
|
||||
}
|
||||
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)
|
||||
|
@ -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
|
||||
|
@ -41,6 +41,7 @@ import { SketchGroup } from '../wasm-lib/kcl/bindings/SketchGroup'
|
||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||
export type { Expr } from '../wasm-lib/kcl/bindings/Expr'
|
||||
export type { ObjectExpression } from '../wasm-lib/kcl/bindings/ObjectExpression'
|
||||
export type { ObjectProperty } from '../wasm-lib/kcl/bindings/ObjectProperty'
|
||||
export type { MemberExpression } from '../wasm-lib/kcl/bindings/MemberExpression'
|
||||
export type { PipeExpression } from '../wasm-lib/kcl/bindings/PipeExpression'
|
||||
export type { VariableDeclaration } from '../wasm-lib/kcl/bindings/VariableDeclaration'
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
@ -633,6 +686,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) {
|
||||
@ -694,9 +795,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
@ -213,7 +213,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,
|
||||
|
@ -0,0 +1,23 @@
|
||||
const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([75.8, 317.2], %)
|
||||
|> 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)
|
||||
const chamf = chamfer({
|
||||
length: 30,
|
||||
tags: [
|
||||
seg01,
|
||||
getNextAdjacentEdge(yo),
|
||||
getNextAdjacentEdge(seg02),
|
||||
getOppositeEdge(seg01)
|
||||
]
|
||||
}, extrude001)
|
@ -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)
|
||||
]
|
||||
}, %)
|
Reference in New Issue
Block a user