#5184 Ability to toggle default planes visibility (#6333)

* add first version of DefaultPlanes to FeatureTreePane

* fix lint issues

* don't show default planes UI in sketch mode

* lint

* toggling default planes: implementation in xstate

* revert malformed modelingMachine.ts

* lint

* save and restore default plane visibility when returning to modeling mode

* fmt

* tsc

* introduce new cleanup state with actor when exiting sketch mode

* temp remove restore default plane visibility - causes error on starting up a project

* set selection filter after executeAst - this is a wip hacky fix

* remove unused early return: this also caused plane selection to only work with double click

* lint

* no need to set selection filter to curves only, we want faces to be selectable in modeling mode, even though this means default planes are also selectable

* tightening types for visibility map

* lint

* cleanups

* fix border issue when visibility toggle is not active and props.visible === true

* ui updates on FeatureTreePane/default planes

* no pointer cursor for unselectable default planes

* show default planes initially even for non-empty projects

* dont show default planes initially when project is not empty

* fix test: Only show axis planes when there are no errors

* fixes for sketch tests

* better initialize for planes

* lint

* fix uneccessary 'reset camera position' in sketch entry

* revert hiding/showing content depending on artifact graph for tests

* only show default planes when there are no errors

* disable Restore default plane visibility, was causing temporary flashing of default planes when exiting sketch mode

* Always show default plane visibility toggles, regardless of being on/off

* revert modelingMachine to original idle states to avoid 'zoom_to_fit' test regression - probably racing condition

* fmt
This commit is contained in:
Andrew Varga
2025-04-25 18:21:19 +02:00
committed by GitHub
parent 885d2afaa3
commit fe22a67cf6
4 changed files with 202 additions and 53 deletions

View File

@ -19,7 +19,6 @@ import { err, reportRejection, trap } from '@src/lib/trap'
import type { IndexLoaderData } from '@src/lib/types'
import { uuidv4 } from '@src/lib/utils'
import { engineStreamActor, useSettings } from '@src/lib/singletons'
import { useCommandBarState } from '@src/lib/singletons'
import {
EngineStreamState,
EngineStreamTransition,
@ -58,8 +57,6 @@ export const EngineStream = (props: {
const { state: modelingMachineState, send: modelingMachineActorSend } =
useModelingContext()
const commandBarState = useCommandBarState()
const streamIdleMode = settings.app.streamIdleMode.current
const startOrReconfigureEngine = () => {
@ -332,15 +329,7 @@ export const EngineStream = (props: {
if (!engineStreamState.context.videoRef.current) return
// If we're in sketch mode, don't send a engine-side select event
if (modelingMachineState.matches('Sketch')) return
// Only respect default plane selection if we're on a selection command argument
if (
modelingMachineState.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
@ -361,7 +350,6 @@ export const EngineStream = (props: {
!isNetworkOkay ||
!engineStreamState.context.videoRef.current ||
modelingMachineState.matches('Sketch') ||
modelingMachineState.matches({ idle: 'showPlanes' }) ||
sceneInfra.camControls.wasDragging === true ||
!btnName(e.nativeEvent).left
) {

View File

@ -1,7 +1,7 @@
import type { Diagnostic } from '@codemirror/lint'
import { useMachine, useSelector } from '@xstate/react'
import type { ComponentProps } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import type { Actor, Prop } from 'xstate'
import type { Operation } from '@rust/kcl-lib/bindings/Operation'
@ -23,7 +23,7 @@ import {
getOperationLabel,
stdLibMap,
} from '@src/lib/operations'
import { editorManager, kclManager } from '@src/lib/singletons'
import { editorManager, kclManager, rustContext } from '@src/lib/singletons'
import {
featureTreeMachine,
featureTreeMachineDefaultContext,
@ -160,6 +160,7 @@ export const FeatureTreePane = () => {
<Loading className="h-full">Building feature tree...</Loading>
) : (
<>
{!modelingState.matches('Sketch') && <DefaultPlanes />}
{parseErrors.length > 0 && (
<div
className={`absolute inset-0 rounded-lg p-2 ${
@ -204,41 +205,27 @@ export const FeatureTreePane = () => {
)
}
export const visibilityMap = new Map<string, boolean>()
interface VisibilityToggleProps {
entityId: string
initialVisibility: boolean
onVisibilityChange?: () => void
visible: boolean
onVisibilityChange: () => unknown
}
/**
* A button that toggles the visibility of an entity
* tied to an artifact in the feature tree.
* TODO: this is unimplemented and will be used for
* default planes after we fix them and add them to the artifact graph / feature tree
* For now just used for default planes.
*/
const VisibilityToggle = (props: VisibilityToggleProps) => {
const [visible, setVisible] = useState(props.initialVisibility)
function handleToggleVisible() {
setVisible(!visible)
visibilityMap.set(props.entityId, !visible)
props.onVisibilityChange?.()
}
const visible = props.visible
const handleToggleVisible = useCallback(() => {
props.onVisibilityChange()
}, [props.onVisibilityChange])
return (
<button
onClick={handleToggleVisible}
className="border-transparent p-0 m-0"
>
<button onClick={handleToggleVisible} className="p-0 m-0">
<CustomIcon
name={visible ? 'eyeOpen' : 'eyeCrossedOut'}
className={`w-5 h-5 ${
visible
? 'hidden group-hover/item:block group-focus-within/item:block'
: 'text-chalkboard-50'
}`}
className="w-5 h-5"
/>
</button>
)
@ -256,6 +243,7 @@ const OperationItemWrapper = ({
menuItems,
errors,
className,
selectable = true,
...props
}: React.HTMLAttributes<HTMLButtonElement> & {
icon: CustomIconName
@ -263,17 +251,18 @@ const OperationItemWrapper = ({
visibilityToggle?: VisibilityToggleProps
menuItems?: ComponentProps<typeof ContextMenu>['items']
errors?: Diagnostic[]
selectable?: 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 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' : ''}`}
>
<button
{...props}
className={`reset flex-1 flex items-center gap-2 border-transparent dark:border-transparent text-left text-base ${className}`}
className={`reset flex-1 flex items-center gap-2 text-left text-base ${selectable ? 'border-transparent dark:border-transparent' : 'border-none cursor-default'} ${className}`}
>
<CustomIcon name={icon} className="w-5 h-5 block" />
{name}
@ -514,3 +503,40 @@ const OperationItem = (props: {
/>
)
}
const DefaultPlanes = () => {
const { state: modelingState, send } = useModelingContext()
const defaultPlanes = rustContext.defaultPlanes
if (!defaultPlanes) return null
const planes = [
{ name: 'Front plane', id: defaultPlanes.xz, key: 'xz' },
{ name: 'Top plane', id: defaultPlanes.xy, key: 'xy' },
{ name: 'Side plane', id: defaultPlanes.yz, key: 'yz' },
] as const
return (
<div className="mb-2">
{planes.map((plane) => (
<OperationItemWrapper
key={plane.key}
icon={'plane'}
name={plane.name}
selectable={false}
visibilityToggle={{
visible: modelingState.context.defaultPlaneVisibility[plane.key],
onVisibilityChange: () => {
send({
type: 'Toggle default plane visibility',
planeId: plane.id,
planeKey: plane.key,
})
},
}}
/>
))}
<div className="h-px bg-chalkboard-50/20 my-2" />
</div>
)
}

View File

@ -48,6 +48,7 @@ import { jsAppSettings } from '@src/lib/settings/settingsUtils'
import { err, reportRejection } from '@src/lib/trap'
import { deferExecution, uuidv4 } from '@src/lib/utils'
import type { PlaneVisibilityMap } from '@src/machines/modelingMachine'
interface ExecuteArgs {
ast?: Node<Program>
@ -70,7 +71,6 @@ export class KclManager {
*/
artifactGraph: ArtifactGraph = new Map()
artifactIndex: ArtifactIndex = []
defaultPlanesShown: boolean = false
private _ast: Node<Program> = {
body: [],
@ -354,6 +354,15 @@ export class KclManager {
})
}, 200)(null)
}
// Send the 'artifact graph initialized' event for modelingMachine, only once, when default planes are also initialized.
deferExecution((a?: null) => {
if (this.defaultPlanes) {
this.engineCommandManager.modelingSend({
type: 'Artifact graph initialized',
})
}
}, 200)(null)
}
async safeParse(code: string): Promise<Node<Program> | null> {
@ -702,6 +711,19 @@ export class KclManager {
}
return Promise.all(thePromises)
}
setPlaneVisibilityByKey(
planeKey: keyof PlaneVisibilityMap,
visible: boolean
) {
const planeId = this.defaultPlanes?.[planeKey]
if (!planeId) {
console.warn(`Plane ${planeKey} not found`)
return
}
return this.engineCommandManager.setPlaneHidden(planeId, !visible)
}
/** TODO: this function is hiding unawaited asynchronous work */
defaultSelectionFilter(selectionsToRestore?: Selections) {
setSelectionFilterToDefault(this.engineCommandManager, selectionsToRestore)

File diff suppressed because one or more lines are too long