Skip to content

Commit

Permalink
Add offset plane point-and-click user flow (#4552)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
franknoirot authored Nov 26, 2024
1 parent 1d45bed commit 4423ae1
Show file tree
Hide file tree
Showing 17 changed files with 389 additions and 83 deletions.
2 changes: 1 addition & 1 deletion e2e/playwright/fixtures/cmdBarFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class CmdBarFixture {
}

private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
const reviewForm = await this.page.locator('#review-form')
const reviewForm = this.page.locator('#review-form')
const getHeaderArgs = async () => {
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
const entries = await Promise.all(
Expand Down
2 changes: 2 additions & 0 deletions e2e/playwright/fixtures/toolbarFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class ToolbarFixture {
public page: Page

extrudeButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
lineBtn!: Locator
rectangleBtn!: Locator
Expand All @@ -25,6 +26,7 @@ export class ToolbarFixture {
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line')
this.rectangleBtn = page.getByTestId('corner-rectangle')
Expand Down
50 changes: 50 additions & 0 deletions e2e/playwright/point-click.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,3 +551,53 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
)
})
})

test(`Offset plane point-and-click`, async ({
app,
scene,
editor,
toolbar,
cmdBar,
}) => {
await app.initialise()

// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)`

await test.step(`Look for the blue of the XZ plane`, async () => {
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
})
await test.step(`Go through the command bar flow`, async () => {
await toolbar.offsetPlaneButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'plane',
currentArgValue: '',
headerArguments: { Plane: '', Distance: '' },
highlightedHeaderArg: 'plane',
commandName: 'Offset plane',
})
await clickOnXzPlane()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'distance',
currentArgValue: '5',
headerArguments: { Plane: '1 plane', Distance: '' },
highlightedHeaderArg: 'distance',
commandName: 'Offset plane',
})
await cmdBar.progressCmdBar()
})

await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(expectedOutput)
await editor.expectState({
diagnostics: [],
activeLines: [expectedOutput],
highlightedCode: '',
})
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
})
})
4 changes: 2 additions & 2 deletions src/clientSideScene/sceneInfra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { Coords2d, compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext'
import * as TWEEN from '@tweenjs/tween.js'
import { Axis } from 'lib/selections'
import { Axis, NonCodeSelection } from 'lib/selections'
import { type BaseUnit } from 'lib/settings/settingsTypes'
import { CameraControls } from './CameraControls'
import { EngineCommandManager } from 'lang/std/engineConnection'
Expand Down Expand Up @@ -654,7 +654,7 @@ export class SceneInfra {
await this.onClickCallback({ mouseEvent, intersects })
}
}
updateOtherSelectionColors = (otherSelections: Axis[]) => {
updateOtherSelectionColors = (otherSelections: NonCodeSelection[]) => {
const axisGroup = this.scene.children.find(
({ userData }) => userData?.type === AXIS_GROUP
)
Expand Down
51 changes: 38 additions & 13 deletions src/components/CommandBar/CommandBarSelectionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { useSelector } from '@xstate/react'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { useKclContext } from 'lang/KclProvider'
import { Artifact } from 'lang/std/artifactGraph'
import { CommandArgument } from 'lib/commandTypes'
import {
canSubmitSelectionArg,
getSelectionType,
getSelectionCountByType,
getSelectionTypeDisplayText,
} from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils'
import { modelingMachine } from 'machines/modelingMachine'
import { useEffect, useMemo, useRef, useState } from 'react'
import { StateFrom } from 'xstate'

const semanticEntityNames: { [key: string]: Array<Artifact['type']> } = {
const semanticEntityNames: {
[key: string]: Array<Artifact['type'] | 'defaultPlane'>
} = {
face: ['wall', 'cap', 'solid2D'],
edge: ['segment', 'sweepEdge', 'edgeCutEdge'],
point: [],
plane: ['defaultPlane'],
}

function getSemanticSelectionType(selectionType: Array<Artifact['type']>) {
Expand Down Expand Up @@ -43,21 +48,13 @@ function CommandBarSelectionInput({
stepBack: () => void
onSubmit: (data: unknown) => void
}) {
const { code } = useKclContext()
const inputRef = useRef<HTMLInputElement>(null)
const { commandBarState, commandBarSend } = useCommandsContext()
const [hasSubmitted, setHasSubmitted] = useState(false)
const selection = useSelector(arg.machineActor, selectionSelector)
const selectionsByType = useMemo(() => {
const selectionRangeEnd = !selection
? null
: selection?.graphSelections[0]?.codeRef?.range[1]
return !selectionRangeEnd || selectionRangeEnd === code.length || !selection
? 'none'
: !selection
? 'none'
: getSelectionType(selection)
}, [selection, code])
return getSelectionCountByType(selection)
}, [selection])
const canSubmitSelection = useMemo<boolean>(
() => canSubmitSelectionArg(selectionsByType, arg),
[selectionsByType]
Expand All @@ -67,6 +64,30 @@ function CommandBarSelectionInput({
inputRef.current?.focus()
}, [selection, inputRef])

// Show the default planes if the selection type is 'plane'
useEffect(() => {
if (arg.selectionTypes.includes('plane') && !canSubmitSelection) {
toSync(() => {
return Promise.all([
kclManager.showPlanes(),
kclManager.setSelectionFilter(['plane', 'object']),
])
}, reportRejection)()
}

return () => {
toSync(() => {
const promises = [
new Promise(() => kclManager.defaultSelectionFilter()),
]
if (!kclManager._isAstEmpty(kclManager.ast)) {
promises.push(kclManager.hidePlanes())
}
return Promise.all(promises)
}, reportRejection)()
}
}, [])

// Fast-forward through this arg if it's marked as skippable
// and we have a valid selection already
useEffect(() => {
Expand Down Expand Up @@ -109,11 +130,15 @@ function CommandBarSelectionInput({
{arg.warningMessage}
</p>
)}
<span data-testid="cmd-bar-arg-name" className="sr-only">
{arg.name}
</span>
<input
id="selection"
name="selection"
ref={inputRef}
required
data-testid="cmd-bar-arg-value"
placeholder="Select an entity with your mouse"
className="absolute inset-0 w-full h-full opacity-0 cursor-default"
onKeyDown={(event) => {
Expand Down
5 changes: 3 additions & 2 deletions src/components/CustomIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -818,15 +818,16 @@ const CustomIconMap = {
),
plane: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="plane"
>
<path
fillRule="evenodd"
clipRule="evenodd"
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"
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"
fill="currentColor"
/>
</svg>
Expand Down
17 changes: 6 additions & 11 deletions src/components/ModelingMachineProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ export const ModelingMachineProvider = ({
})
})
}

let selections: Selections = {
graphSelections: [],
otherSelections: [],
Expand Down Expand Up @@ -375,7 +376,10 @@ export const ModelingMachineProvider = ({
}
}

if (setSelections.selectionType === 'otherSelection') {
if (
setSelections.selectionType === 'axisSelection' ||
setSelections.selectionType === 'defaultPlaneSelection'
) {
if (editorManager.isShiftDown) {
selections = {
graphSelections: selectionRanges.graphSelections,
Expand All @@ -387,20 +391,11 @@ export const ModelingMachineProvider = ({
otherSelections: [setSelections.selection],
}
}
const { engineEvents, updateSceneObjectColors } =
handleSelectionBatch({
selections: selections,
})
engineEvents &&
engineEvents.forEach((event) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand(event)
})
updateSceneObjectColors()
return {
selectionRanges: selections,
}
}

if (setSelections.selectionType === 'completeSelection') {
editorManager.selectRange(setSelections.selection)
if (!sketchDetails)
Expand Down
12 changes: 11 additions & 1 deletion src/components/Stream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
import { useCommandsContext } from 'hooks/useCommandsContext'

enum StreamState {
Playing = 'playing',
Expand All @@ -30,6 +31,7 @@ export const Stream = () => {
const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext()
const { state, send } = useModelingContext()
const { commandBarState } = useCommandsContext()
const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext()
const [streamState, setStreamState] = useState(StreamState.Unset)
Expand Down Expand Up @@ -260,7 +262,15 @@ export const Stream = () => {
if (!videoRef.current) return
// If we're in sketch mode, don't send a engine-side select event
if (state.matches('Sketch')) return
if (state.matches({ idle: 'showPlanes' })) return
// Only respect default plane selection if we're on a selection command argument
if (
state.matches({ idle: 'showPlanes' }) &&
!(
commandBarState.matches('Gathering arguments') &&
commandBarState.context.currentArgument?.inputType === 'selection'
)
)
return
// If we're mousing up from a camera drag, don't send a select event
if (sceneInfra.camControls.wasDragging === true) return

Expand Down
1 change: 1 addition & 0 deletions src/hooks/useEngineConnectionSubscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export function useEngineConnectionSubscriptions() {
pathToNode: artifact.codeRef.pathToNode,
},
})
return
}

// Artifact is likely an extrusion face
Expand Down
47 changes: 35 additions & 12 deletions src/lang/KclSingleton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
import { markOnce } from 'lib/performance'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { EntityType_type } from '@kittycad/lib/dist/types/src/models'

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

sceneInfra.modelingSend({ type: 'code edit during sketch' })
defaultSelectionFilter(execState.memory, this.engineCommandManager)
setSelectionFilterToDefault(execState.memory, this.engineCommandManager)

if (args.zoomToFit) {
let zoomObjectId: string | undefined = ''
Expand Down Expand Up @@ -568,8 +569,13 @@ export class KclManager {
}
return Promise.all(thePromises)
}
/** TODO: this function is hiding unawaited asynchronous work */
defaultSelectionFilter() {
defaultSelectionFilter(this.programMemory, this.engineCommandManager)
setSelectionFilterToDefault(this.programMemory, this.engineCommandManager)
}
/** TODO: this function is hiding unawaited asynchronous work */
setSelectionFilter(filter: EntityType_type[]) {
setSelectionFilter(filter, this.engineCommandManager)
}

/**
Expand All @@ -591,18 +597,35 @@ export class KclManager {
}
}

function defaultSelectionFilter(
const defaultSelectionFilter: EntityType_type[] = [
'face',
'edge',
'solid2d',
'curve',
'object',
]

/** TODO: This function is not synchronous but is currently treated as such */
function setSelectionFilterToDefault(
programMemory: ProgramMemory,
engineCommandManager: EngineCommandManager
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
programMemory.hasSketchOrSolid() &&
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter: ['face', 'edge', 'solid2d', 'curve'],
},
})
setSelectionFilter(defaultSelectionFilter, engineCommandManager)
}

/** TODO: This function is not synchronous but is currently treated as such */
function setSelectionFilter(
filter: EntityType_type[],
engineCommandManager: EngineCommandManager
) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'set_selection_filter',
filter,
},
})
}
Loading

0 comments on commit 4423ae1

Please sign in to comment.