Skip to content

Commit 4423ae1

Browse files
authored
Add offset plane point-and-click user flow (#4552)
* Add a code mod for offset plane * Add support for default plane selections to our `otherSelections` object * Make availableVars work without a selection range (because default planes don't have one) * Make default planes selectable in cmdbar even if AST is empty * Add offset plane command and activate in toolbar * Avoid unnecessary error when sketching on offset plane by returning early * Add supporting test features for offset plane E2E test * Add WIP E2E test for offset plane Struggling to get local electron test suite running properly * Typos * Lints * Fix test by making it a web-based one: I couldn't use the cmdBar fixture with an electron test for some reason. * Update src/lib/commandBarConfigs/modelingCommandConfig.ts * Update src/machines/modelingMachine.ts * Revert changes to `homePageFixture`, as they were unused * @Irev-Dev feedback: convert action to actor, fix machine layout * Update plane icon to be not dashed, follow conventions closer
1 parent 1d45bed commit 4423ae1

17 files changed

+389
-83
lines changed

e2e/playwright/fixtures/cmdBarFixture.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class CmdBarFixture {
3535
}
3636

3737
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
38-
const reviewForm = await this.page.locator('#review-form')
38+
const reviewForm = this.page.locator('#review-form')
3939
const getHeaderArgs = async () => {
4040
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
4141
const entries = await Promise.all(

e2e/playwright/fixtures/toolbarFixture.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export class ToolbarFixture {
66
public page: Page
77

88
extrudeButton!: Locator
9+
offsetPlaneButton!: Locator
910
startSketchBtn!: Locator
1011
lineBtn!: Locator
1112
rectangleBtn!: Locator
@@ -25,6 +26,7 @@ export class ToolbarFixture {
2526
reConstruct = (page: Page) => {
2627
this.page = page
2728
this.extrudeButton = page.getByTestId('extrude')
29+
this.offsetPlaneButton = page.getByTestId('plane-offset')
2830
this.startSketchBtn = page.getByTestId('sketch')
2931
this.lineBtn = page.getByTestId('line')
3032
this.rectangleBtn = page.getByTestId('corner-rectangle')

e2e/playwright/point-click.spec.ts

+50
Original file line numberDiff line numberDiff line change
@@ -551,3 +551,53 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
551551
)
552552
})
553553
})
554+
555+
test(`Offset plane point-and-click`, async ({
556+
app,
557+
scene,
558+
editor,
559+
toolbar,
560+
cmdBar,
561+
}) => {
562+
await app.initialise()
563+
564+
// One dumb hardcoded screen pixel value
565+
const testPoint = { x: 700, y: 150 }
566+
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
567+
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
568+
569+
await test.step(`Look for the blue of the XZ plane`, async () => {
570+
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
571+
})
572+
await test.step(`Go through the command bar flow`, async () => {
573+
await toolbar.offsetPlaneButton.click()
574+
await cmdBar.expectState({
575+
stage: 'arguments',
576+
currentArgKey: 'plane',
577+
currentArgValue: '',
578+
headerArguments: { Plane: '', Distance: '' },
579+
highlightedHeaderArg: 'plane',
580+
commandName: 'Offset plane',
581+
})
582+
await clickOnXzPlane()
583+
await cmdBar.expectState({
584+
stage: 'arguments',
585+
currentArgKey: 'distance',
586+
currentArgValue: '5',
587+
headerArguments: { Plane: '1 plane', Distance: '' },
588+
highlightedHeaderArg: 'distance',
589+
commandName: 'Offset plane',
590+
})
591+
await cmdBar.progressCmdBar()
592+
})
593+
594+
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
595+
await editor.expectEditor.toContain(expectedOutput)
596+
await editor.expectState({
597+
diagnostics: [],
598+
activeLines: [expectedOutput],
599+
highlightedCode: '',
600+
})
601+
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
602+
})
603+
})

src/clientSideScene/sceneInfra.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
2323
import { useModelingContext } from 'hooks/useModelingContext'
2424
import * as TWEEN from '@tweenjs/tween.js'
25-
import { Axis } from 'lib/selections'
25+
import { Axis, NonCodeSelection } from 'lib/selections'
2626
import { type BaseUnit } from 'lib/settings/settingsTypes'
2727
import { CameraControls } from './CameraControls'
2828
import { EngineCommandManager } from 'lang/std/engineConnection'
@@ -654,7 +654,7 @@ export class SceneInfra {
654654
await this.onClickCallback({ mouseEvent, intersects })
655655
}
656656
}
657-
updateOtherSelectionColors = (otherSelections: Axis[]) => {
657+
updateOtherSelectionColors = (otherSelections: NonCodeSelection[]) => {
658658
const axisGroup = this.scene.children.find(
659659
({ userData }) => userData?.type === AXIS_GROUP
660660
)

src/components/CommandBar/CommandBarSelectionInput.tsx

+38-13
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import { useSelector } from '@xstate/react'
22
import { useCommandsContext } from 'hooks/useCommandsContext'
3-
import { useKclContext } from 'lang/KclProvider'
43
import { Artifact } from 'lang/std/artifactGraph'
54
import { CommandArgument } from 'lib/commandTypes'
65
import {
76
canSubmitSelectionArg,
8-
getSelectionType,
7+
getSelectionCountByType,
98
getSelectionTypeDisplayText,
109
} from 'lib/selections'
10+
import { kclManager } from 'lib/singletons'
11+
import { reportRejection } from 'lib/trap'
12+
import { toSync } from 'lib/utils'
1113
import { modelingMachine } from 'machines/modelingMachine'
1214
import { useEffect, useMemo, useRef, useState } from 'react'
1315
import { StateFrom } from 'xstate'
1416

15-
const semanticEntityNames: { [key: string]: Array<Artifact['type']> } = {
17+
const semanticEntityNames: {
18+
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
19+
} = {
1620
face: ['wall', 'cap', 'solid2D'],
1721
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
1822
point: [],
23+
plane: ['defaultPlane'],
1924
}
2025

2126
function getSemanticSelectionType(selectionType: Array<Artifact['type']>) {
@@ -43,21 +48,13 @@ function CommandBarSelectionInput({
4348
stepBack: () => void
4449
onSubmit: (data: unknown) => void
4550
}) {
46-
const { code } = useKclContext()
4751
const inputRef = useRef<HTMLInputElement>(null)
4852
const { commandBarState, commandBarSend } = useCommandsContext()
4953
const [hasSubmitted, setHasSubmitted] = useState(false)
5054
const selection = useSelector(arg.machineActor, selectionSelector)
5155
const selectionsByType = useMemo(() => {
52-
const selectionRangeEnd = !selection
53-
? null
54-
: selection?.graphSelections[0]?.codeRef?.range[1]
55-
return !selectionRangeEnd || selectionRangeEnd === code.length || !selection
56-
? 'none'
57-
: !selection
58-
? 'none'
59-
: getSelectionType(selection)
60-
}, [selection, code])
56+
return getSelectionCountByType(selection)
57+
}, [selection])
6158
const canSubmitSelection = useMemo<boolean>(
6259
() => canSubmitSelectionArg(selectionsByType, arg),
6360
[selectionsByType]
@@ -67,6 +64,30 @@ function CommandBarSelectionInput({
6764
inputRef.current?.focus()
6865
}, [selection, inputRef])
6966

67+
// Show the default planes if the selection type is 'plane'
68+
useEffect(() => {
69+
if (arg.selectionTypes.includes('plane') && !canSubmitSelection) {
70+
toSync(() => {
71+
return Promise.all([
72+
kclManager.showPlanes(),
73+
kclManager.setSelectionFilter(['plane', 'object']),
74+
])
75+
}, reportRejection)()
76+
}
77+
78+
return () => {
79+
toSync(() => {
80+
const promises = [
81+
new Promise(() => kclManager.defaultSelectionFilter()),
82+
]
83+
if (!kclManager._isAstEmpty(kclManager.ast)) {
84+
promises.push(kclManager.hidePlanes())
85+
}
86+
return Promise.all(promises)
87+
}, reportRejection)()
88+
}
89+
}, [])
90+
7091
// Fast-forward through this arg if it's marked as skippable
7192
// and we have a valid selection already
7293
useEffect(() => {
@@ -109,11 +130,15 @@ function CommandBarSelectionInput({
109130
{arg.warningMessage}
110131
</p>
111132
)}
133+
<span data-testid="cmd-bar-arg-name" className="sr-only">
134+
{arg.name}
135+
</span>
112136
<input
113137
id="selection"
114138
name="selection"
115139
ref={inputRef}
116140
required
141+
data-testid="cmd-bar-arg-value"
117142
placeholder="Select an entity with your mouse"
118143
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
119144
onKeyDown={(event) => {

src/components/CustomIcon.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -818,15 +818,16 @@ const CustomIconMap = {
818818
),
819819
plane: (
820820
<svg
821+
width="20"
822+
height="20"
821823
viewBox="0 0 20 20"
822824
fill="none"
823825
xmlns="http://www.w3.org/2000/svg"
824-
aria-label="plane"
825826
>
826827
<path
827828
fillRule="evenodd"
828829
clipRule="evenodd"
829-
d="M4.92871 5.11391L4.43964 5.00995V4.10898V3.60898V3.10898L4.92871 3.21293L5.41778 3.31689L6.29907 3.50421V4.00421V4.50421L5.41778 4.31689V5.21786L4.92871 5.11391ZM11.8774 4.68991L8.1585 3.89945V4.39945V4.89945L11.8774 5.68991V5.18991V4.68991ZM13.7368 5.08515V5.58515V6.08515L14.6181 6.27247V7.17344L15.1071 7.2774L15.5962 7.38135V6.48038V5.98038V5.48038L15.1071 5.37643L14.6181 5.27247L13.7368 5.08515ZM15.5962 9.28233L15.1071 9.17837L14.6181 9.07441V12.8764L15.1071 12.9803L15.5962 13.0843V9.28233ZM15.5962 14.9852L15.1071 14.8813L14.6181 14.7773V15.6783L13.7368 15.491V15.991V16.491L14.6181 16.6783L15.1071 16.7823L15.5962 16.8862V16.3862V15.8862V14.9852ZM11.8774 16.0957V15.5957V15.0957L8.1585 14.3053V14.8053V15.3053L11.8774 16.0957ZM6.29907 14.91V14.41V13.91L5.41778 13.7227V12.8217L4.92871 12.7178L4.43964 12.6138V13.5148V14.0148V14.5148L4.92871 14.6188L5.41778 14.7227L6.29907 14.91ZM4.43964 10.7129L4.92871 10.8168L5.41778 10.9208V7.11883L4.92871 7.01488L4.43964 6.91092V10.7129Z"
830+
d="M10.9781 5.49876L14.6181 6.27247V9.99381L10.9781 9.22011V5.49876ZM10 4.29085L10.9781 4.49876L14.6181 5.27247L14.6182 5.27247L15.5963 5.48038H15.5963V6.48038V10.2017V11.2017L15.5963 11.2017V15.8862V16.8862L14.6181 16.6783L5.41784 14.7227L4.4397 14.5148V13.5148V4.10898V3.10898L5.41784 3.31689L10 4.29085ZM14.6181 10.9938V15.6783L5.41784 13.7227V4.31689L10 5.29085V9.0122V10.0122L10.9781 10.2201L14.6181 10.9938Z"
830831
fill="currentColor"
831832
/>
832833
</svg>

src/components/ModelingMachineProvider.tsx

+6-11
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ export const ModelingMachineProvider = ({
317317
})
318318
})
319319
}
320+
320321
let selections: Selections = {
321322
graphSelections: [],
322323
otherSelections: [],
@@ -375,7 +376,10 @@ export const ModelingMachineProvider = ({
375376
}
376377
}
377378

378-
if (setSelections.selectionType === 'otherSelection') {
379+
if (
380+
setSelections.selectionType === 'axisSelection' ||
381+
setSelections.selectionType === 'defaultPlaneSelection'
382+
) {
379383
if (editorManager.isShiftDown) {
380384
selections = {
381385
graphSelections: selectionRanges.graphSelections,
@@ -387,20 +391,11 @@ export const ModelingMachineProvider = ({
387391
otherSelections: [setSelections.selection],
388392
}
389393
}
390-
const { engineEvents, updateSceneObjectColors } =
391-
handleSelectionBatch({
392-
selections: selections,
393-
})
394-
engineEvents &&
395-
engineEvents.forEach((event) => {
396-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
397-
engineCommandManager.sendSceneCommand(event)
398-
})
399-
updateSceneObjectColors()
400394
return {
401395
selectionRanges: selections,
402396
}
403397
}
398+
404399
if (setSelections.selectionType === 'completeSelection') {
405400
editorManager.selectRange(setSelections.selection)
406401
if (!sketchDetails)

src/components/Stream.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { useRouteLoaderData } from 'react-router-dom'
1818
import { PATHS } from 'lib/paths'
1919
import { IndexLoaderData } from 'lib/types'
20+
import { useCommandsContext } from 'hooks/useCommandsContext'
2021

2122
enum StreamState {
2223
Playing = 'playing',
@@ -30,6 +31,7 @@ export const Stream = () => {
3031
const videoRef = useRef<HTMLVideoElement>(null)
3132
const { settings } = useSettingsAuthContext()
3233
const { state, send } = useModelingContext()
34+
const { commandBarState } = useCommandsContext()
3335
const { mediaStream } = useAppStream()
3436
const { overallState, immediateState } = useNetworkContext()
3537
const [streamState, setStreamState] = useState(StreamState.Unset)
@@ -260,7 +262,15 @@ export const Stream = () => {
260262
if (!videoRef.current) return
261263
// If we're in sketch mode, don't send a engine-side select event
262264
if (state.matches('Sketch')) return
263-
if (state.matches({ idle: 'showPlanes' })) return
265+
// Only respect default plane selection if we're on a selection command argument
266+
if (
267+
state.matches({ idle: 'showPlanes' }) &&
268+
!(
269+
commandBarState.matches('Gathering arguments') &&
270+
commandBarState.context.currentArgument?.inputType === 'selection'
271+
)
272+
)
273+
return
264274
// If we're mousing up from a camera drag, don't send a select event
265275
if (sceneInfra.camControls.wasDragging === true) return
266276

src/hooks/useEngineConnectionSubscriptions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ export function useEngineConnectionSubscriptions() {
169169
pathToNode: artifact.codeRef.pathToNode,
170170
},
171171
})
172+
return
172173
}
173174

174175
// Artifact is likely an extrusion face

src/lang/KclSingleton.ts

+35-12
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
2323
import { Diagnostic } from '@codemirror/lint'
2424
import { markOnce } from 'lib/performance'
2525
import { Node } from 'wasm-lib/kcl/bindings/Node'
26+
import { EntityType_type } from '@kittycad/lib/dist/types/src/models'
2627

2728
interface ExecuteArgs {
2829
ast?: Node<Program>
@@ -281,7 +282,7 @@ export class KclManager {
281282
this.lints = await lintAst({ ast: ast })
282283

283284
sceneInfra.modelingSend({ type: 'code edit during sketch' })
284-
defaultSelectionFilter(execState.memory, this.engineCommandManager)
285+
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)
285286

286287
if (args.zoomToFit) {
287288
let zoomObjectId: string | undefined = ''
@@ -568,8 +569,13 @@ export class KclManager {
568569
}
569570
return Promise.all(thePromises)
570571
}
572+
/** TODO: this function is hiding unawaited asynchronous work */
571573
defaultSelectionFilter() {
572-
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
574+
setSelectionFilterToDefault(this.programMemory, this.engineCommandManager)
575+
}
576+
/** TODO: this function is hiding unawaited asynchronous work */
577+
setSelectionFilter(filter: EntityType_type[]) {
578+
setSelectionFilter(filter, this.engineCommandManager)
573579
}
574580

575581
/**
@@ -591,18 +597,35 @@ export class KclManager {
591597
}
592598
}
593599

594-
function defaultSelectionFilter(
600+
const defaultSelectionFilter: EntityType_type[] = [
601+
'face',
602+
'edge',
603+
'solid2d',
604+
'curve',
605+
'object',
606+
]
607+
608+
/** TODO: This function is not synchronous but is currently treated as such */
609+
function setSelectionFilterToDefault(
595610
programMemory: ProgramMemory,
596611
engineCommandManager: EngineCommandManager
597612
) {
598613
// eslint-disable-next-line @typescript-eslint/no-floating-promises
599-
programMemory.hasSketchOrSolid() &&
600-
engineCommandManager.sendSceneCommand({
601-
type: 'modeling_cmd_req',
602-
cmd_id: uuidv4(),
603-
cmd: {
604-
type: 'set_selection_filter',
605-
filter: ['face', 'edge', 'solid2d', 'curve'],
606-
},
607-
})
614+
setSelectionFilter(defaultSelectionFilter, engineCommandManager)
615+
}
616+
617+
/** TODO: This function is not synchronous but is currently treated as such */
618+
function setSelectionFilter(
619+
filter: EntityType_type[],
620+
engineCommandManager: EngineCommandManager
621+
) {
622+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
623+
engineCommandManager.sendSceneCommand({
624+
type: 'modeling_cmd_req',
625+
cmd_id: uuidv4(),
626+
cmd: {
627+
type: 'set_selection_filter',
628+
filter,
629+
},
630+
})
608631
}

0 commit comments

Comments
 (0)