[BUG] Grab bag of sketch mode bug fixes (#6229)
* use artifact id for sketch mode entry plane * fix up re-eval as best as possible * remove some async dodgyness * fmt * fix old sycronous re-execute shit * add a test * fix existing test * add toast for error state * spelling * test stuff * fmt * fix toast * test fix * some other fix ups * fix test
This commit is contained in:
@ -3,7 +3,7 @@ import * as fsp from 'fs/promises'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
test.describe('Import UI tests', () => {
|
test.describe('Import UI tests', () => {
|
||||||
test('shows toast when trying to sketch on imported face', async ({
|
test('shows toast when trying to sketch on imported face, and hovering over imported geometry should NOT highlight any code', async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
@ -93,7 +93,6 @@ sketch002 = startSketchOn(extrude001, face = seg01)`
|
|||||||
await toolbar.startSketchPlaneSelection()
|
await toolbar.startSketchPlaneSelection()
|
||||||
|
|
||||||
// Click on a face from the imported model
|
// Click on a face from the imported model
|
||||||
// await new Promise(() => {})
|
|
||||||
await importedFaceClick()
|
await importedFaceClick()
|
||||||
|
|
||||||
// Verify toast appears with correct content
|
// Verify toast appears with correct content
|
||||||
|
@ -1363,73 +1363,6 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test.describe('Sketch mode should be toleratant to syntax errors', () => {
|
|
||||||
test(
|
|
||||||
'adding a syntax error, recovers after fixing',
|
|
||||||
{ tag: ['@skipWin'] },
|
|
||||||
async ({ page, homePage, context, scene, editor, toolbar }) => {
|
|
||||||
const file = await fs.readFile(
|
|
||||||
path.resolve(
|
|
||||||
__dirname,
|
|
||||||
'../../',
|
|
||||||
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
|
|
||||||
),
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
await context.addInitScript((file) => {
|
|
||||||
localStorage.setItem('persistCode', file)
|
|
||||||
}, file)
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
|
|
||||||
const [objClick] = scene.makeMouseHelpers(600, 250)
|
|
||||||
const arrowHeadLocation = { x: 706, y: 129 } as const
|
|
||||||
const arrowHeadWhite = TEST_COLORS.WHITE
|
|
||||||
const backgroundGray: [number, number, number] = [28, 28, 28]
|
|
||||||
const verifyArrowHeadColor = async (c: [number, number, number]) =>
|
|
||||||
scene.expectPixelColor(c, arrowHeadLocation, 15)
|
|
||||||
|
|
||||||
await test.step('check chamfer selection changes cursor positon', async () => {
|
|
||||||
await expect(async () => {
|
|
||||||
// sometimes initial click doesn't register
|
|
||||||
await objClick()
|
|
||||||
await editor.expectActiveLinesToBe([
|
|
||||||
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
|
|
||||||
])
|
|
||||||
}).toPass({ timeout: 15_000, intervals: [500] })
|
|
||||||
})
|
|
||||||
|
|
||||||
await test.step('enter sketch and sanity check segments have been drawn', async () => {
|
|
||||||
await toolbar.editSketch()
|
|
||||||
// this checks sketch segments have been drawn
|
|
||||||
await verifyArrowHeadColor(arrowHeadWhite)
|
|
||||||
})
|
|
||||||
|
|
||||||
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
|
|
||||||
await editor.replaceCode('line(endAbsolute = [pro', 'badBadBadFn([pro')
|
|
||||||
await editor.expectState({
|
|
||||||
activeLines: [],
|
|
||||||
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
|
|
||||||
highlightedCode: '',
|
|
||||||
})
|
|
||||||
// this checks sketch segments have failed to be drawn
|
|
||||||
await verifyArrowHeadColor(backgroundGray)
|
|
||||||
})
|
|
||||||
|
|
||||||
await test.step('', async () => {
|
|
||||||
await editor.replaceCode('badBadBadFn([pro', 'line(endAbsolute = [pro')
|
|
||||||
await editor.expectState({
|
|
||||||
activeLines: [],
|
|
||||||
diagnostics: [],
|
|
||||||
highlightedCode: '',
|
|
||||||
})
|
|
||||||
// this checks sketch segments have been drawn
|
|
||||||
await verifyArrowHeadColor(arrowHeadWhite)
|
|
||||||
})
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test.describe(`Sketching with offset planes`, () => {
|
test.describe(`Sketching with offset planes`, () => {
|
||||||
test(`Can select an offset plane to sketch on`, async ({
|
test(`Can select an offset plane to sketch on`, async ({
|
||||||
context,
|
context,
|
||||||
@ -1623,6 +1556,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
|||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`@settings(defaultLengthUnit = in)
|
`@settings(defaultLengthUnit = in)
|
||||||
|
|
||||||
sketch001 = startSketchOn(XZ)
|
sketch001 = startSketchOn(XZ)
|
||||||
profile002 = startProfileAt([40.68, 87.67], sketch001)
|
profile002 = startProfileAt([40.68, 87.67], sketch001)
|
||||||
|> xLine(length = 239.17)
|
|> xLine(length = 239.17)
|
||||||
@ -3109,3 +3043,312 @@ profile001 = startProfileAt([0, 0], sketch001)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('manual edits during sketch mode', () => {
|
||||||
|
test('Can edit sketch through feature tree with variable modifications', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `myVar1 = 5
|
||||||
|
myVar2 = 6
|
||||||
|
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = startProfileAt([106.68, 89.77], sketch001)
|
||||||
|
|> line(end = [132.34, 157.8])
|
||||||
|
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude001 = extrude(profile001, length = 500)
|
||||||
|
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||||
|
profile002 = startProfileAt([83.39, 329.15], sketch002)
|
||||||
|
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 156.54, angle = -28)
|
||||||
|
|> angledLine(
|
||||||
|
angle = segAng(rectangleSegmentA001),
|
||||||
|
length = -segLen(rectangleSegmentA001),
|
||||||
|
angle = -151,
|
||||||
|
length = 116.27,
|
||||||
|
)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|
||||||
|
|> line(end = [103.55, 33.32])
|
||||||
|
|> line(end = [48.8, -153.54])`
|
||||||
|
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.connectionEstablished()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
const expectSketchOriginToBeDrawn = async () => {
|
||||||
|
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step('Open feature tree and edit second sketch', async () => {
|
||||||
|
await toolbar.openFeatureTreePane()
|
||||||
|
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
|
||||||
|
await sketchButton.dblclick()
|
||||||
|
await page.waitForTimeout(700) // Wait for engine animation
|
||||||
|
await expectSketchOriginToBeDrawn()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Add new variable and wait for re-execution', async () => {
|
||||||
|
await page.waitForTimeout(500) // wait for deferred execution
|
||||||
|
await editor.replaceCode('myVar2 = 6', 'myVar2 = 6\nmyVar3 = 7')
|
||||||
|
await page.waitForTimeout(2000) // wait for deferred execution
|
||||||
|
await expectSketchOriginToBeDrawn()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handle1Location = { x: 843, y: 235 }
|
||||||
|
|
||||||
|
await test.step('Edit sketch by dragging handle', async () => {
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await editor.expectEditor.toContain('length = 156.54, angle = -28')
|
||||||
|
await page.mouse.move(handle1Location.x, handle1Location.y)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(handle1Location.x + 50, handle1Location.y + 50, {
|
||||||
|
steps: 5,
|
||||||
|
})
|
||||||
|
await page.mouse.up()
|
||||||
|
await editor.expectEditor.toContain('length = 231.59, angle = -34')
|
||||||
|
// await page.waitForTimeout(1000) // Wait for update
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Delete variables and wait for re-execution', async () => {
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await editor.replaceCode('myVar3 = 7', '')
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await editor.replaceCode('myVar2 = 6', '')
|
||||||
|
await page.waitForTimeout(2000) // Wait for deferred execution
|
||||||
|
await expectSketchOriginToBeDrawn()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handle2Location = { x: 872, y: 273 }
|
||||||
|
await test.step('Edit sketch again', async () => {
|
||||||
|
await editor.expectEditor.toContain('length = 231.59, angle = -34')
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await page.mouse.move(handle2Location.x, handle2Location.y)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(handle2Location.x, handle2Location.y - 50, {
|
||||||
|
steps: 5,
|
||||||
|
})
|
||||||
|
await page.mouse.up()
|
||||||
|
await editor.expectEditor.toContain('length = 167.36, angle = -14')
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('add whole other sketch before current sketch', async () => {
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await editor.replaceCode(
|
||||||
|
`myVar1 = 5`,
|
||||||
|
`myVar1 = 5
|
||||||
|
sketch003 = startSketchOn(XY)
|
||||||
|
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)`
|
||||||
|
)
|
||||||
|
await page.waitForTimeout(2000) // Wait for deferred execution
|
||||||
|
await expectSketchOriginToBeDrawn()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handle3Location = { x: 844, y: 212 }
|
||||||
|
await test.step('edit sketch again', async () => {
|
||||||
|
await editor.expectEditor.toContain('length = 167.36, angle = -14')
|
||||||
|
await page.mouse.move(handle3Location.x, handle3Location.y)
|
||||||
|
await page.mouse.down()
|
||||||
|
await page.mouse.move(handle3Location.x, handle3Location.y + 110, {
|
||||||
|
steps: 5,
|
||||||
|
})
|
||||||
|
await page.mouse.up()
|
||||||
|
await editor.expectEditor.toContain('length = 219.2, angle = -56')
|
||||||
|
})
|
||||||
|
|
||||||
|
// exit sketch and assert whole code
|
||||||
|
await test.step('Exit sketch and assert code', async () => {
|
||||||
|
await toolbar.exitSketch()
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`myVar1 = 5
|
||||||
|
sketch003 = startSketchOn(XY)
|
||||||
|
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)
|
||||||
|
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = startProfileAt([106.68, 89.77], sketch001)
|
||||||
|
|> line(end = [132.34, 157.8])
|
||||||
|
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude001 = extrude(profile001, length = 500)
|
||||||
|
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||||
|
profile002 = startProfileAt([83.39, 329.15], sketch002)
|
||||||
|
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 219.2, angle = -56)
|
||||||
|
|> angledLine(
|
||||||
|
angle = segAng(rectangleSegmentA001),
|
||||||
|
length = -segLen(rectangleSegmentA001),
|
||||||
|
angle = -151,
|
||||||
|
length = 116.27,
|
||||||
|
)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|
||||||
|
|> line(end = [103.55, 33.32])
|
||||||
|
|> line(end = [48.8, -153.54])
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
await editor.expectState({
|
||||||
|
activeLines: [],
|
||||||
|
diagnostics: [],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test('Will exit out of sketch mode for some incompatible edits', async ({
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `myVar1 = 5
|
||||||
|
myVar2 = 6
|
||||||
|
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = startProfileAt([106.68, 89.77], sketch001)
|
||||||
|
|> line(end = [132.34, 157.8])
|
||||||
|
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
extrude001 = extrude(profile001, length = 500)
|
||||||
|
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||||
|
profile002 = startProfileAt([83.39, 329.15], sketch002)
|
||||||
|
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 156.54, angle = -28)
|
||||||
|
|> angledLine(
|
||||||
|
angle = segAng(rectangleSegmentA001),
|
||||||
|
length = -segLen(rectangleSegmentA001),
|
||||||
|
angle = -151,
|
||||||
|
length = 116.27,
|
||||||
|
)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|
||||||
|
|> line(end = [103.55, 33.32])
|
||||||
|
|> line(end = [48.8, -153.54])`
|
||||||
|
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.connectionEstablished()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
const expectSketchOriginToBeDrawn = async () => {
|
||||||
|
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step('Open feature tree and edit second sketch', async () => {
|
||||||
|
await toolbar.openFeatureTreePane()
|
||||||
|
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
|
||||||
|
await sketchButton.dblclick()
|
||||||
|
await page.waitForTimeout(700) // Wait for engine animation
|
||||||
|
await expectSketchOriginToBeDrawn()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('rename variable of current sketch, sketch002 to changeSketchNamePartWayThrough', async () => {
|
||||||
|
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
// three times to rename the declaration and it's use
|
||||||
|
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
|
||||||
|
await expect(
|
||||||
|
page.getByText('Unable to maintain sketch mode')
|
||||||
|
).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
test(
|
||||||
|
'adding a syntax error, recovers after fixing',
|
||||||
|
{ tag: ['@skipWin'] },
|
||||||
|
async ({ page, homePage, context, scene, editor, toolbar, cmdBar }) => {
|
||||||
|
const file = await fs.readFile(
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../',
|
||||||
|
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
|
||||||
|
),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
await context.addInitScript((file) => {
|
||||||
|
localStorage.setItem('persistCode', file)
|
||||||
|
}, file)
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
const [objClick] = scene.makeMouseHelpers(600, 250)
|
||||||
|
const arrowHeadLocation = { x: 706, y: 129 } as const
|
||||||
|
const arrowHeadWhite = TEST_COLORS.WHITE
|
||||||
|
const backgroundGray: [number, number, number] = [28, 28, 28]
|
||||||
|
const verifyArrowHeadColor = async (c: [number, number, number]) =>
|
||||||
|
scene.expectPixelColor(c, arrowHeadLocation, 15)
|
||||||
|
|
||||||
|
// wait for scene to load
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await test.step('check chamfer selection changes cursor positon', async () => {
|
||||||
|
await expect(async () => {
|
||||||
|
// sometimes initial click doesn't register
|
||||||
|
await objClick()
|
||||||
|
await editor.expectActiveLinesToBe([
|
||||||
|
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
|
||||||
|
])
|
||||||
|
}).toPass({ timeout: 15_000, intervals: [500] })
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('enter sketch and sanity check segments have been drawn', async () => {
|
||||||
|
await toolbar.editSketch()
|
||||||
|
// this checks sketch segments have been drawn
|
||||||
|
await verifyArrowHeadColor(arrowHeadWhite)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
|
||||||
|
await editor.replaceCode(
|
||||||
|
'line(endAbsolute = [pro',
|
||||||
|
'badBadBadFn(endAbsolute = [pro'
|
||||||
|
)
|
||||||
|
await editor.expectState({
|
||||||
|
activeLines: [],
|
||||||
|
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
page.getByText(
|
||||||
|
"Error in kcl script, sketch cannot be drawn until it's fixed"
|
||||||
|
)
|
||||||
|
).toBeVisible()
|
||||||
|
// this checks sketch segments have failed to be drawn
|
||||||
|
await verifyArrowHeadColor(backgroundGray)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('', async () => {
|
||||||
|
await editor.replaceCode(
|
||||||
|
'badBadBadFn(endAbsolute = [pro',
|
||||||
|
'line(endAbsolute = [pro'
|
||||||
|
)
|
||||||
|
await editor.expectState({
|
||||||
|
activeLines: [],
|
||||||
|
diagnostics: [],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
// this checks sketch segments have been drawn
|
||||||
|
await verifyArrowHeadColor(arrowHeadWhite)
|
||||||
|
})
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@ -551,6 +551,9 @@ export const ModelingMachineProvider = ({
|
|||||||
if (selectionRanges.graphSelections.length <= 0) return false
|
if (selectionRanges.graphSelections.length <= 0) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
'is-error-free': () => {
|
||||||
|
return kclManager.errors.length === 0 && !kclManager.hasErrors()
|
||||||
|
},
|
||||||
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
||||||
if (event.type !== 'Enter sketch') return false
|
if (event.type !== 'Enter sketch') return false
|
||||||
if (event.data?.forceNewSketch) return false
|
if (event.data?.forceNewSketch) return false
|
||||||
@ -854,6 +857,7 @@ export const ModelingMachineProvider = ({
|
|||||||
: plane?.pathIds[0]
|
: plane?.pathIds[0]
|
||||||
let sketch: KclValue | null = null
|
let sketch: KclValue | null = null
|
||||||
let planeVar: Plane | null = null
|
let planeVar: Plane | null = null
|
||||||
|
|
||||||
for (const variable of Object.values(
|
for (const variable of Object.values(
|
||||||
kclManager.execState.variables
|
kclManager.execState.variables
|
||||||
)) {
|
)) {
|
||||||
@ -884,6 +888,7 @@ export const ModelingMachineProvider = ({
|
|||||||
planeVar = variable.value
|
planeVar = variable.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sketch || sketch.type !== 'Sketch') {
|
if (!sketch || sketch.type !== 'Sketch') {
|
||||||
if (artifact?.type !== 'plane')
|
if (artifact?.type !== 'plane')
|
||||||
return Promise.reject(new Error('No sketch'))
|
return Promise.reject(new Error('No sketch'))
|
||||||
@ -1911,18 +1916,6 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
}, [modelingActor])
|
}, [modelingActor])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
kclManager.registerExecuteCallback(() => {
|
|
||||||
modelingSend({ type: 'Re-execute' })
|
|
||||||
})
|
|
||||||
|
|
||||||
// Before this component unmounts, call the 'Cancel'
|
|
||||||
// event to clean up any state in the modeling machine.
|
|
||||||
return () => {
|
|
||||||
modelingSend({ type: 'Cancel' })
|
|
||||||
}
|
|
||||||
}, [modelingSend])
|
|
||||||
|
|
||||||
// Give the state back to the editorManager.
|
// Give the state back to the editorManager.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editorManager.modelingSend = modelingSend
|
editorManager.modelingSend = modelingSend
|
||||||
|
@ -200,9 +200,8 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
)
|
)
|
||||||
if (!err(extrusion)) {
|
if (!err(extrusion)) {
|
||||||
if (!isTopLevelModule(extrusion.codeRef.range)) {
|
if (!isTopLevelModule(extrusion.codeRef.range)) {
|
||||||
const fileIndex = getModuleId(extrusion.codeRef.range)
|
const moduleId = getModuleId(extrusion.codeRef.range)
|
||||||
const importDetails =
|
const importDetails = kclManager.execState.filenames[moduleId]
|
||||||
kclManager.execState.filenames[fileIndex]
|
|
||||||
if (!importDetails) {
|
if (!importDetails) {
|
||||||
toast.error("can't sketch on this face")
|
toast.error("can't sketch on this face")
|
||||||
return
|
return
|
||||||
@ -217,6 +216,7 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
) {
|
) {
|
||||||
toast.error("can't sketch on this face")
|
toast.error("can't sketch on this face")
|
||||||
} else {
|
} else {
|
||||||
|
// force tsc error if more cases are added
|
||||||
const _exhaustiveCheck: never = importDetails
|
const _exhaustiveCheck: never = importDetails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ import type {
|
|||||||
ExecState,
|
ExecState,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
Program,
|
Program,
|
||||||
SourceRange,
|
|
||||||
VariableMap,
|
VariableMap,
|
||||||
} from '@src/lang/wasm'
|
} from '@src/lang/wasm'
|
||||||
import { emptyExecState, getKclVersion, parse, recast } from '@src/lang/wasm'
|
import { emptyExecState, getKclVersion, parse, recast } from '@src/lang/wasm'
|
||||||
@ -48,7 +47,7 @@ import type {
|
|||||||
import { jsAppSettings } from '@src/lib/settings/settingsUtils'
|
import { jsAppSettings } from '@src/lib/settings/settingsUtils'
|
||||||
|
|
||||||
import { err, reportRejection } from '@src/lib/trap'
|
import { err, reportRejection } from '@src/lib/trap'
|
||||||
import { deferExecution, isOverlap, uuidv4 } from '@src/lib/utils'
|
import { deferExecution, uuidv4 } from '@src/lib/utils'
|
||||||
|
|
||||||
interface ExecuteArgs {
|
interface ExecuteArgs {
|
||||||
ast?: Node<Program>
|
ast?: Node<Program>
|
||||||
@ -88,6 +87,21 @@ export class KclManager {
|
|||||||
preComments: [],
|
preComments: [],
|
||||||
commentStart: 0,
|
commentStart: 0,
|
||||||
}
|
}
|
||||||
|
_lastAst: Node<Program> = {
|
||||||
|
body: [],
|
||||||
|
shebang: null,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
moduleId: 0,
|
||||||
|
nonCodeMeta: {
|
||||||
|
nonCodeNodes: {},
|
||||||
|
startNodes: [],
|
||||||
|
},
|
||||||
|
innerAttrs: [],
|
||||||
|
outerAttrs: [],
|
||||||
|
preComments: [],
|
||||||
|
commentStart: 0,
|
||||||
|
}
|
||||||
private _execState: ExecState = emptyExecState()
|
private _execState: ExecState = emptyExecState()
|
||||||
private _variables: VariableMap = {}
|
private _variables: VariableMap = {}
|
||||||
lastSuccessfulVariables: VariableMap = {}
|
lastSuccessfulVariables: VariableMap = {}
|
||||||
@ -115,13 +129,16 @@ export class KclManager {
|
|||||||
private _kclErrorsCallBack: (errors: KCLError[]) => void = () => {}
|
private _kclErrorsCallBack: (errors: KCLError[]) => void = () => {}
|
||||||
private _diagnosticsCallback: (errors: Diagnostic[]) => void = () => {}
|
private _diagnosticsCallback: (errors: Diagnostic[]) => void = () => {}
|
||||||
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
private _wasmInitFailedCallback: (arg: boolean) => void = () => {}
|
||||||
private _executeCallback: () => void = () => {}
|
|
||||||
sceneInfraBaseUnitMultiplierSetter: (unit: BaseUnit) => void = () => {}
|
sceneInfraBaseUnitMultiplierSetter: (unit: BaseUnit) => void = () => {}
|
||||||
|
|
||||||
get ast() {
|
get ast() {
|
||||||
return this._ast
|
return this._ast
|
||||||
}
|
}
|
||||||
set ast(ast) {
|
set ast(ast) {
|
||||||
|
if (this._ast.body.length !== 0) {
|
||||||
|
// last intact ast, if the user makes a typo with a syntax error, we want to keep the one before they made that mistake
|
||||||
|
this._lastAst = structuredClone(this._ast)
|
||||||
|
}
|
||||||
this._ast = ast
|
this._ast = ast
|
||||||
this._astCallBack(ast)
|
this._astCallBack(ast)
|
||||||
}
|
}
|
||||||
@ -242,6 +259,8 @@ export class KclManager {
|
|||||||
await this.safeParse(this.singletons.codeManager.code).then((ast) => {
|
await this.safeParse(this.singletons.codeManager.code).then((ast) => {
|
||||||
if (ast) {
|
if (ast) {
|
||||||
this.ast = ast
|
this.ast = ast
|
||||||
|
// on setup, set _lastAst so it's populated.
|
||||||
|
this._lastAst = ast
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -272,9 +291,6 @@ export class KclManager {
|
|||||||
this._isExecutingCallback = setIsExecuting
|
this._isExecutingCallback = setIsExecuting
|
||||||
this._wasmInitFailedCallback = setWasmInitFailed
|
this._wasmInitFailedCallback = setWasmInitFailed
|
||||||
}
|
}
|
||||||
registerExecuteCallback(callback: () => void) {
|
|
||||||
this._executeCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAst() {
|
clearAst() {
|
||||||
this._ast = {
|
this._ast = {
|
||||||
@ -338,24 +354,6 @@ export class KclManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some "objects" have the same source range, such as sketch_mode_start and start_path.
|
|
||||||
// So when passing a range, we need to also specify the command type
|
|
||||||
private mapRangeToObjectId(
|
|
||||||
range: SourceRange,
|
|
||||||
commandTypeToTarget: string
|
|
||||||
): string | undefined {
|
|
||||||
for (const [artifactId, artifact] of this.artifactGraph) {
|
|
||||||
if (
|
|
||||||
'codeRef' in artifact &&
|
|
||||||
artifact.codeRef &&
|
|
||||||
isOverlap(range, artifact.codeRef.range)
|
|
||||||
) {
|
|
||||||
if (commandTypeToTarget === artifact.type) return artifactId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
async safeParse(code: string): Promise<Node<Program> | null> {
|
async safeParse(code: string): Promise<Node<Program> | null> {
|
||||||
const result = parse(code)
|
const result = parse(code)
|
||||||
this.diagnostics = []
|
this.diagnostics = []
|
||||||
@ -474,10 +472,9 @@ export class KclManager {
|
|||||||
this.lastSuccessfulVariables = execState.variables
|
this.lastSuccessfulVariables = execState.variables
|
||||||
this.lastSuccessfulOperations = execState.operations
|
this.lastSuccessfulOperations = execState.operations
|
||||||
}
|
}
|
||||||
this.ast = { ...ast }
|
this.ast = structuredClone(ast)
|
||||||
// updateArtifactGraph relies on updated executeState/variables
|
// updateArtifactGraph relies on updated executeState/variables
|
||||||
await this.updateArtifactGraph(execState.artifactGraph)
|
await this.updateArtifactGraph(execState.artifactGraph)
|
||||||
this._executeCallback()
|
|
||||||
if (!isInterrupted) {
|
if (!isInterrupted) {
|
||||||
this.singletons.sceneInfra.modelingSend({
|
this.singletons.sceneInfra.modelingSend({
|
||||||
type: 'code edit during sketch',
|
type: 'code edit during sketch',
|
||||||
@ -557,8 +554,7 @@ export class KclManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ast = { ...ast }
|
return this.executeAst({ ast })
|
||||||
return this.executeAst()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async format() {
|
async format() {
|
||||||
|
@ -996,3 +996,59 @@ function pathToNodeKeys(pathToNode: PathToNode): (string | number)[] {
|
|||||||
export function stringifyPathToNode(pathToNode: PathToNode): string {
|
export function stringifyPathToNode(pathToNode: PathToNode): string {
|
||||||
return JSON.stringify(pathToNodeKeys(pathToNode))
|
return JSON.stringify(pathToNodeKeys(pathToNode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates PathToNodes to account for changes in body indices when variables are added/removed above
|
||||||
|
* This is specifically for handling the common case where a user adds/removes variables above a node,
|
||||||
|
* which changes the body index in the PathToNode
|
||||||
|
* @param oldAst The AST before user edits
|
||||||
|
* @param newAst The AST after user edits
|
||||||
|
* @param pathToUpdate Array of PathToNodes that need to be updated
|
||||||
|
* @returns updated PathToNode, or Error if any path couldn't be updated
|
||||||
|
*/
|
||||||
|
export function updatePathToNodesAfterEdit(
|
||||||
|
oldAst: Node<Program>,
|
||||||
|
newAst: Node<Program>,
|
||||||
|
pathToUpdate: PathToNode
|
||||||
|
): PathToNode | Error {
|
||||||
|
// First, let's find all topLevel the variable declarations in both ASTs
|
||||||
|
// and map their name to their body index
|
||||||
|
const oldVarDecls = new Map<string, number>()
|
||||||
|
const newVarDecls = new Map<string, number>()
|
||||||
|
|
||||||
|
const maxBodyLength = Math.max(oldAst.body.length, newAst.body.length)
|
||||||
|
for (let bodyIndex = 0; bodyIndex < maxBodyLength; bodyIndex++) {
|
||||||
|
const oldNode = oldAst.body[bodyIndex]
|
||||||
|
const newNode = newAst.body[bodyIndex]
|
||||||
|
if (oldNode?.type === 'VariableDeclaration') {
|
||||||
|
oldVarDecls.set(oldNode.declaration.id.name, bodyIndex)
|
||||||
|
}
|
||||||
|
if (newNode?.type === 'VariableDeclaration') {
|
||||||
|
newVarDecls.set(newNode.declaration.id.name, bodyIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the path, get the variable name this path points to
|
||||||
|
const oldNodeResult = getNodeFromPath<VariableDeclaration>(
|
||||||
|
oldAst,
|
||||||
|
pathToUpdate,
|
||||||
|
'VariableDeclaration'
|
||||||
|
)
|
||||||
|
if (err(oldNodeResult)) return oldNodeResult
|
||||||
|
const oldNode = oldNodeResult.node
|
||||||
|
const varName = oldNode.declaration.id.name
|
||||||
|
console.log('varName', varName)
|
||||||
|
|
||||||
|
// Find the old and new indices for this variable
|
||||||
|
const oldIndex = oldVarDecls.get(varName)
|
||||||
|
const newIndex = newVarDecls.get(varName)
|
||||||
|
|
||||||
|
if (oldIndex === undefined || newIndex === undefined) {
|
||||||
|
return new Error(`Could not find variable ${varName} in one of the ASTs`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new path with the updated body index
|
||||||
|
const newPath = structuredClone(pathToUpdate)
|
||||||
|
newPath[1][0] = newIndex // Update the body index
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
@ -738,7 +738,7 @@ const onlyConsecutivePaths = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPathsFromPlaneArtifact(
|
export function getPathsFromPlaneArtifact(
|
||||||
planeArtifact: PlaneArtifact,
|
planeArtifact: PlaneArtifact | WallArtifact | CapArtifact,
|
||||||
artifactGraph: ArtifactGraph,
|
artifactGraph: ArtifactGraph,
|
||||||
ast: Program
|
ast: Program
|
||||||
): PathToNode[] {
|
): PathToNode[] {
|
||||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user