Skip to content

Right click a feature on the model to center the code pane on it #7447 #7664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions e2e/playwright/sketch-tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,68 @@ sketch001 = startProfile(sketch002, at = [12.34, -12.34])
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})

test('Can select planes in Feature Tree after Start Sketch', async ({
page,
homePage,
toolbar,
editor,
}) => {
// Load the app with empty code
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`plane001 = offsetPlane(XZ, offset = 5)`
)
})

await page.setBodyDimensions({ width: 1200, height: 500 })

await homePage.goToModelingScene()

await test.step('Click Start Sketch button', async () => {
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
await expect(page.getByText('select a plane or face')).toBeVisible()
})

await test.step('Open feature tree and select Front plane (XZ)', async () => {
await toolbar.openFeatureTreePane()

await page.getByRole('button', { name: 'Front plane' }).click()

await page.waitForTimeout(600)

await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(XZ)')

await page.getByRole('button', { name: 'Exit Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeVisible()
})

await test.step('Click Start Sketch button again', async () => {
await page.getByRole('button', { name: 'Start Sketch' }).click()
await expect(
page.getByRole('button', { name: 'Exit Sketch' })
).toBeVisible()
})

await test.step('Select the offset plane', async () => {
await toolbar.openFeatureTreePane()

await page.getByRole('button', { name: 'Offset plane' }).click()

await page.waitForTimeout(600)

await expect(toolbar.lineBtn).toBeEnabled()
await editor.expectEditor.toContain('startSketchOn(plane001)')
})
})

test('Can edit segments by dragging their handles', () => {
const doEditSegmentsByDraggingHandle = async (
page: Page,
Expand Down
135 changes: 122 additions & 13 deletions src/components/ModelingSidebar/ModelingPanes/FeatureTreePane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ import {
getOperationVariableName,
stdLibMap,
} from '@src/lib/operations'
import { editorManager, kclManager, rustContext } from '@src/lib/singletons'
import {
editorManager,
kclManager,
rustContext,
sceneInfra,
} from '@src/lib/singletons'
import {
featureTreeMachine,
featureTreeMachineDefaultContext,
Expand All @@ -34,11 +39,20 @@ import {
kclEditorActor,
selectionEventSelector,
} from '@src/machines/kclEditorMachine'
import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
import {
selectDefaultSketchPlane,
selectOffsetSketchPlane,
} from '@src/lib/selections'
import type { DefaultPlaneStr } from '@src/lib/planes'

export const FeatureTreePane = () => {
const isEditorMounted = useSelector(kclEditorActor, editorIsMountedSelector)
const lastSelectionEvent = useSelector(kclEditorActor, selectionEventSelector)
const { send: modelingSend, state: modelingState } = useModelingContext()

const sketchNoFace = modelingState.matches('Sketch no face')

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_featureTreeState, featureTreeSend] = useMachine(
featureTreeMachine.provide({
Expand Down Expand Up @@ -195,6 +209,7 @@ export const FeatureTreePane = () => {
key={key}
item={operation}
send={featureTreeSend}
sketchNoFace={sketchNoFace}
/>
)
})}
Expand Down Expand Up @@ -251,6 +266,7 @@ const OperationItemWrapper = ({
customSuffix,
className,
selectable = true,
greyedOut = false,
...props
}: React.HTMLAttributes<HTMLButtonElement> & {
icon: CustomIconName
Expand All @@ -262,18 +278,19 @@ const OperationItemWrapper = ({
menuItems?: ComponentProps<typeof ContextMenu>['items']
errors?: Diagnostic[]
selectable?: boolean
greyedOut?: boolean
}) => {
const menuRef = useRef<HTMLDivElement>(null)

return (
<div
ref={menuRef}
className={`flex select-none items-center group/item my-0 py-0.5 px-1 ${selectable ? 'focus-within:bg-primary/10 hover:bg-primary/5' : ''}`}
className={`flex select-none items-center group/item my-0 py-0.5 px-1 ${selectable ? 'focus-within:bg-primary/10 hover:bg-primary/5' : ''} ${greyedOut ? 'opacity-50 cursor-not-allowed' : ''}`}
data-testid="feature-tree-operation-item"
>
<button
{...props}
className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : 'border-none cursor-default'} ${className}`}
className={`reset !py-0.5 !px-1 flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : '!border-transparent cursor-default'} ${className}`}
>
<CustomIcon name={icon} className="w-5 h-5 block" />
<div className="flex flex-1 items-baseline align-baseline">
Expand Down Expand Up @@ -311,6 +328,7 @@ const OperationItemWrapper = ({
const OperationItem = (props: {
item: Operation
send: Prop<Actor<typeof featureTreeMachine>, 'send'>
sketchNoFace: boolean
}) => {
const kclContext = useKclContext()
const name = getOperationLabel(props.item)
Expand Down Expand Up @@ -343,15 +361,22 @@ const OperationItem = (props: {
}, [kclContext.diagnostics.length])

function selectOperation() {
if (props.item.type === 'GroupEnd') {
return
if (props.sketchNoFace) {
if (isOffsetPlane(props.item)) {
const artifact = findOperationArtifact(props.item)
void selectOffsetSketchPlane(artifact)
}
} else {
if (props.item.type === 'GroupEnd') {
return
}
props.send({
type: 'selectOperation',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
},
})
}
props.send({
type: 'selectOperation',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
},
})
}

/**
Expand Down Expand Up @@ -432,6 +457,20 @@ const OperationItem = (props: {
}
}

function startSketchOnOffsetPlane() {
if (isOffsetPlane(props.item)) {
const artifact = findOperationArtifact(props.item)
if (artifact?.id) {
sceneInfra.modelingSend({
type: 'Enter sketch',
data: { forceNewSketch: true },
})

void selectOffsetSketchPlane(artifact)
}
}
}

const menuItems = useMemo(
() => [
<ContextMenuItem
Expand Down Expand Up @@ -477,6 +516,13 @@ const OperationItem = (props: {
</ContextMenuItem>,
]
: []),
...(isOffsetPlane(props.item)
? [
<ContextMenuItem onClick={startSketchOnOffsetPlane}>
Start Sketch
</ContextMenuItem>,
]
: []),
...(props.item.type === 'StdLibCall' ||
props.item.type === 'VariableDeclaration'
? [
Expand Down Expand Up @@ -550,22 +596,63 @@ const OperationItem = (props: {
[props.item, props.send]
)

const enabled = !props.sketchNoFace || isOffsetPlane(props.item)

return (
<OperationItemWrapper
selectable={enabled}
icon={getOperationIcon(props.item)}
name={name}
variableName={variableName}
valueDetail={valueDetail}
menuItems={menuItems}
onClick={selectOperation}
onDoubleClick={enterEditFlow}
onDoubleClick={props.sketchNoFace ? undefined : enterEditFlow} // no double click in "Sketch no face" mode
errors={errors}
greyedOut={!enabled}
/>
)
}

const DefaultPlanes = () => {
const { state: modelingState, send } = useModelingContext()
const sketchNoFace = modelingState.matches('Sketch no face')

const onClickPlane = useCallback(
(planeId: string) => {
if (sketchNoFace) {
selectDefaultSketchPlane(planeId)
} else {
const foundDefaultPlane =
rustContext.defaultPlanes !== null &&
Object.entries(rustContext.defaultPlanes).find(
([, plane]) => plane === planeId
)
if (foundDefaultPlane) {
send({
type: 'Set selection',
data: {
selectionType: 'defaultPlaneSelection',
selection: {
name: foundDefaultPlane[0] as DefaultPlaneStr,
id: planeId,
},
},
})
}
}
},
[sketchNoFace]
)

const startSketchOnDefaultPlane = useCallback((planeId: string) => {
sceneInfra.modelingSend({
type: 'Enter sketch',
data: { forceNewSketch: true },
})

selectDefaultSketchPlane(planeId)
}, [])

const defaultPlanes = rustContext.defaultPlanes
if (!defaultPlanes) return null
Expand Down Expand Up @@ -603,7 +690,15 @@ const DefaultPlanes = () => {
customSuffix={plane.customSuffix}
icon={'plane'}
name={plane.name}
selectable={false}
selectable={true}
onClick={() => onClickPlane(plane.id)}
menuItems={[
<ContextMenuItem
onClick={() => startSketchOnDefaultPlane(plane.id)}
>
Start Sketch
</ContextMenuItem>,
]}
visibilityToggle={{
visible: modelingState.context.defaultPlaneVisibility[plane.key],
onVisibilityChange: () => {
Expand All @@ -620,3 +715,17 @@ const DefaultPlanes = () => {
</div>
)
}

type StdLibCallOp = Extract<Operation, { type: 'StdLibCall' }>

const isOffsetPlane = (item: Operation): item is StdLibCallOp => {
return item.type === 'StdLibCall' && item.name === 'offsetPlane'
}

const findOperationArtifact = (item: StdLibCallOp) => {
const nodePath = JSON.stringify(item.nodePath)
const artifact = [...kclManager.artifactGraph.values()].find(
(a) => JSON.stringify((a as Plane).codeRef?.nodePath) === nodePath
)
return artifact
}
Loading