Hook up chamfer UI with AST-mod (#4694)
* button * config * hook up with ast * cmd bar test * button states fix and test * little naming fix * xState action to actor * remove button state test updates * fixture-based approach * nightly Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> * Update src/lib/toolbar.ts Co-authored-by: Frank Noirot <frank@zoo.dev> --------- Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> Co-authored-by: Frank Noirot <frank@zoo.dev>
This commit is contained in:
@ -15,6 +15,7 @@ export class ToolbarFixture {
|
|||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
loftButton!: Locator
|
loftButton!: Locator
|
||||||
sweepButton!: Locator
|
sweepButton!: Locator
|
||||||
|
chamferButton!: Locator
|
||||||
shellButton!: Locator
|
shellButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
@ -42,6 +43,7 @@ export class ToolbarFixture {
|
|||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.loftButton = page.getByTestId('loft')
|
this.loftButton = page.getByTestId('loft')
|
||||||
this.sweepButton = page.getByTestId('sweep')
|
this.sweepButton = page.getByTestId('sweep')
|
||||||
|
this.chamferButton = page.getByTestId('chamfer3d')
|
||||||
this.shellButton = page.getByTestId('shell')
|
this.shellButton = page.getByTestId('shell')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
|
@ -1032,6 +1032,222 @@ sketch002 = startSketchOn('XZ')
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Chamfer point-and-click`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
// TODO: fix this test on windows after the electron migration
|
||||||
|
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||||
|
|
||||||
|
// Code samples
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XY')
|
||||||
|
|> startProfileAt([-12, -6], %)
|
||||||
|
|> line([0, 12], %)
|
||||||
|
|> line([24, 0], %)
|
||||||
|
|> line([0, -12], %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||||
|
|> close(%)
|
||||||
|
extrude001 = extrude(-12, sketch001)
|
||||||
|
`
|
||||||
|
const firstChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
|
||||||
|
const secondChamferDeclaration =
|
||||||
|
'chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, %)'
|
||||||
|
|
||||||
|
// Locators
|
||||||
|
const firstEdgeLocation = { x: 600, y: 193 }
|
||||||
|
const secondEdgeLocation = { x: 600, y: 383 }
|
||||||
|
const bodyLocation = { x: 630, y: 290 }
|
||||||
|
const [clickOnFirstEdge] = scene.makeMouseHelpers(
|
||||||
|
firstEdgeLocation.x,
|
||||||
|
firstEdgeLocation.y
|
||||||
|
)
|
||||||
|
const [clickOnSecondEdge] = scene.makeMouseHelpers(
|
||||||
|
secondEdgeLocation.x,
|
||||||
|
secondEdgeLocation.y
|
||||||
|
)
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||||
|
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
|
||||||
|
const bodyColor: [number, number, number] = [155, 155, 155]
|
||||||
|
const chamferColor: [number, number, number] = [168, 168, 168]
|
||||||
|
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||||
|
const lowTolerance = 20
|
||||||
|
const highTolerance = 40
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
await test.step(`Verify scene is loaded`, async () => {
|
||||||
|
// verify modeling scene is loaded
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
backgroundColor,
|
||||||
|
secondEdgeLocation,
|
||||||
|
lowTolerance
|
||||||
|
)
|
||||||
|
|
||||||
|
// wait for stream to load
|
||||||
|
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 1: Command bar flow with preselected edges
|
||||||
|
await test.step(`Select first edge`, async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
firstEdgeLocation,
|
||||||
|
lowTolerance
|
||||||
|
)
|
||||||
|
await clickOnFirstEdge()
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
firstEdgeLocation,
|
||||||
|
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
||||||
|
await toolbar.chamferButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
highlightedHeaderArg: 'length',
|
||||||
|
currentArgKey: 'length',
|
||||||
|
currentArgValue: '5',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 face',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 face',
|
||||||
|
Length: '5',
|
||||||
|
},
|
||||||
|
stage: 'review',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor`, async () => {
|
||||||
|
await editor.expectEditor.toContain(firstChamferDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: ['|>chamfer({length=5,tags=[seg01]},%)'],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm scene has changed`, async () => {
|
||||||
|
await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 2: Command bar flow without preselected edges
|
||||||
|
await test.step(`Open chamfer UI without selecting edges`, async () => {
|
||||||
|
await toolbar.chamferButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Select second edge`, async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorWhite,
|
||||||
|
secondEdgeLocation,
|
||||||
|
lowTolerance
|
||||||
|
)
|
||||||
|
await clickOnSecondEdge()
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
edgeColorYellow,
|
||||||
|
secondEdgeLocation,
|
||||||
|
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Apply chamfer to the second edge`, async () => {
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
highlightedHeaderArg: 'length',
|
||||||
|
currentArgKey: 'length',
|
||||||
|
currentArgValue: '5',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 sweepEdge',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 sweepEdge',
|
||||||
|
Length: '5',
|
||||||
|
},
|
||||||
|
stage: 'review',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor`, async () => {
|
||||||
|
await editor.expectEditor.toContain(secondChamferDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: ['length=5,'],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm scene has changed`, async () => {
|
||||||
|
await scene.expectPixelColor(
|
||||||
|
backgroundColor,
|
||||||
|
secondEdgeLocation,
|
||||||
|
lowTolerance
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const shellPointAndClickCapCases = [
|
const shellPointAndClickCapCases = [
|
||||||
{ shouldPreselect: true },
|
{ shouldPreselect: true },
|
||||||
{ shouldPreselect: false },
|
{ shouldPreselect: false },
|
||||||
|
@ -56,10 +56,13 @@ export type ModelingCommandSchema = {
|
|||||||
edge: Selections
|
edge: Selections
|
||||||
}
|
}
|
||||||
Fillet: {
|
Fillet: {
|
||||||
// todo
|
|
||||||
selection: Selections
|
selection: Selections
|
||||||
radius: KclCommandValue
|
radius: KclCommandValue
|
||||||
}
|
}
|
||||||
|
Chamfer: {
|
||||||
|
selection: Selections
|
||||||
|
length: KclCommandValue
|
||||||
|
}
|
||||||
'Offset plane': {
|
'Offset plane': {
|
||||||
plane: Selections
|
plane: Selections
|
||||||
distance: KclCommandValue
|
distance: KclCommandValue
|
||||||
@ -429,7 +432,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
Fillet: {
|
Fillet: {
|
||||||
description: 'Fillet edge',
|
description: 'Fillet edge',
|
||||||
icon: 'fillet',
|
icon: 'fillet3d',
|
||||||
status: 'development',
|
status: 'development',
|
||||||
needsReview: true,
|
needsReview: true,
|
||||||
args: {
|
args: {
|
||||||
@ -449,6 +452,28 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Chamfer: {
|
||||||
|
description: 'Chamfer edge',
|
||||||
|
icon: 'chamfer3d',
|
||||||
|
status: 'development',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||||
|
multiple: true,
|
||||||
|
required: true,
|
||||||
|
skip: false,
|
||||||
|
warningMessage:
|
||||||
|
'Chamfers cannot touch other chamfers yet. This is under development.',
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_LENGTH,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'Constrain length': {
|
'Constrain length': {
|
||||||
description: 'Constrain the length of one or more segments.',
|
description: 'Constrain the length of one or more segments.',
|
||||||
icon: 'dimension',
|
icon: 'dimension',
|
||||||
|
@ -173,10 +173,14 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/fillet' }],
|
links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/fillet' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chamfer',
|
id: 'chamfer3d',
|
||||||
onClick: () => console.error('Chamfer not yet implemented'),
|
onClick: ({ commandBarSend }) =>
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Chamfer', groupId: 'modeling' },
|
||||||
|
}),
|
||||||
icon: 'chamfer3d',
|
icon: 'chamfer3d',
|
||||||
status: 'kcl-only',
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||||
title: 'Chamfer',
|
title: 'Chamfer',
|
||||||
hotkey: 'C',
|
hotkey: 'C',
|
||||||
description: 'Bevel the edges of a 3D solid.',
|
description: 'Bevel the edges of a 3D solid.',
|
||||||
|
@ -52,6 +52,7 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
applyEdgeTreatmentToSelection,
|
applyEdgeTreatmentToSelection,
|
||||||
|
ChamferParameters,
|
||||||
EdgeTreatmentType,
|
EdgeTreatmentType,
|
||||||
FilletParameters,
|
FilletParameters,
|
||||||
} from 'lang/modifyAst/addEdgeTreatment'
|
} from 'lang/modifyAst/addEdgeTreatment'
|
||||||
@ -272,6 +273,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
|
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
|
||||||
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
||||||
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
||||||
|
| { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
|
||||||
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
||||||
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
|
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
|
||||||
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
|
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
|
||||||
@ -1737,6 +1739,33 @@ export const modelingMachine = setup({
|
|||||||
if (err(filletResult)) return filletResult
|
if (err(filletResult)) return filletResult
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
chamferAstMod: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['Chamfer'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) {
|
||||||
|
return new Error('No input provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract inputs
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const { selection, length } = input
|
||||||
|
const parameters: ChamferParameters = {
|
||||||
|
type: EdgeTreatmentType.Chamfer,
|
||||||
|
length,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply chamfer to selection
|
||||||
|
const chamferResult = await applyEdgeTreatmentToSelection(
|
||||||
|
ast,
|
||||||
|
selection,
|
||||||
|
parameters
|
||||||
|
)
|
||||||
|
if (err(chamferResult)) return chamferResult
|
||||||
|
}
|
||||||
|
),
|
||||||
'submit-prompt-edit': fromPromise(
|
'submit-prompt-edit': fromPromise(
|
||||||
async ({ input }: { input: ModelingCommandSchema['Prompt-to-edit'] }) => {
|
async ({ input }: { input: ModelingCommandSchema['Prompt-to-edit'] }) => {
|
||||||
console.log('doing thing', input)
|
console.log('doing thing', input)
|
||||||
@ -1821,6 +1850,11 @@ export const modelingMachine = setup({
|
|||||||
reenter: true,
|
reenter: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Chamfer: {
|
||||||
|
target: 'Applying chamfer',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
|
||||||
Export: {
|
Export: {
|
||||||
target: 'idle',
|
target: 'idle',
|
||||||
reenter: false,
|
reenter: false,
|
||||||
@ -2650,6 +2684,19 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Applying chamfer': {
|
||||||
|
invoke: {
|
||||||
|
src: 'chamferAstMod',
|
||||||
|
id: 'chamferAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Chamfer') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: ['idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
'Applying Prompt-to-edit': {
|
'Applying Prompt-to-edit': {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'submit-prompt-edit',
|
src: 'submit-prompt-edit',
|
||||||
|
Reference in New Issue
Block a user