@@ -24,7 +24,12 @@ import {
24
24
getOperationVariableName ,
25
25
stdLibMap ,
26
26
} from '@src/lib/operations'
27
- import { editorManager , kclManager , rustContext } from '@src/lib/singletons'
27
+ import {
28
+ editorManager ,
29
+ kclManager ,
30
+ rustContext ,
31
+ sceneInfra ,
32
+ } from '@src/lib/singletons'
28
33
import {
29
34
featureTreeMachine ,
30
35
featureTreeMachineDefaultContext ,
@@ -34,11 +39,20 @@ import {
34
39
kclEditorActor ,
35
40
selectionEventSelector ,
36
41
} from '@src/machines/kclEditorMachine'
42
+ import type { Plane } from '@rust/kcl-lib/bindings/Artifact'
43
+ import {
44
+ selectDefaultSketchPlane ,
45
+ selectOffsetSketchPlane ,
46
+ } from '@src/lib/selections'
47
+ import type { DefaultPlaneStr } from '@src/lib/planes'
37
48
38
49
export const FeatureTreePane = ( ) => {
39
50
const isEditorMounted = useSelector ( kclEditorActor , editorIsMountedSelector )
40
51
const lastSelectionEvent = useSelector ( kclEditorActor , selectionEventSelector )
41
52
const { send : modelingSend , state : modelingState } = useModelingContext ( )
53
+
54
+ const sketchNoFace = modelingState . matches ( 'Sketch no face' )
55
+
42
56
// eslint-disable-next-line @typescript-eslint/no-unused-vars
43
57
const [ _featureTreeState , featureTreeSend ] = useMachine (
44
58
featureTreeMachine . provide ( {
@@ -195,6 +209,7 @@ export const FeatureTreePane = () => {
195
209
key = { key }
196
210
item = { operation }
197
211
send = { featureTreeSend }
212
+ sketchNoFace = { sketchNoFace }
198
213
/>
199
214
)
200
215
} ) }
@@ -251,6 +266,7 @@ const OperationItemWrapper = ({
251
266
customSuffix,
252
267
className,
253
268
selectable = true ,
269
+ greyedOut = false ,
254
270
...props
255
271
} : React . HTMLAttributes < HTMLButtonElement > & {
256
272
icon : CustomIconName
@@ -262,18 +278,19 @@ const OperationItemWrapper = ({
262
278
menuItems ?: ComponentProps < typeof ContextMenu > [ 'items' ]
263
279
errors ?: Diagnostic [ ]
264
280
selectable ?: boolean
281
+ greyedOut ?: boolean
265
282
} ) => {
266
283
const menuRef = useRef < HTMLDivElement > ( null )
267
284
268
285
return (
269
286
< div
270
287
ref = { menuRef }
271
- 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' : '' } ` }
288
+ 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' : '' } ` }
272
289
data-testid = "feature-tree-operation-item"
273
290
>
274
291
< button
275
292
{ ...props }
276
- 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 } ` }
293
+ 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 } ` }
277
294
>
278
295
< CustomIcon name = { icon } className = "w-5 h-5 block" />
279
296
< div className = "flex flex-1 items-baseline align-baseline" >
@@ -311,6 +328,7 @@ const OperationItemWrapper = ({
311
328
const OperationItem = ( props : {
312
329
item : Operation
313
330
send : Prop < Actor < typeof featureTreeMachine > , 'send' >
331
+ sketchNoFace : boolean
314
332
} ) => {
315
333
const kclContext = useKclContext ( )
316
334
const name = getOperationLabel ( props . item )
@@ -343,15 +361,22 @@ const OperationItem = (props: {
343
361
} , [ kclContext . diagnostics . length ] )
344
362
345
363
function selectOperation ( ) {
346
- if ( props . item . type === 'GroupEnd' ) {
347
- return
364
+ if ( props . sketchNoFace ) {
365
+ if ( isOffsetPlane ( props . item ) ) {
366
+ const artifact = findOperationArtifact ( props . item )
367
+ void selectOffsetSketchPlane ( artifact )
368
+ }
369
+ } else {
370
+ if ( props . item . type === 'GroupEnd' ) {
371
+ return
372
+ }
373
+ props . send ( {
374
+ type : 'selectOperation' ,
375
+ data : {
376
+ targetSourceRange : sourceRangeFromRust ( props . item . sourceRange ) ,
377
+ } ,
378
+ } )
348
379
}
349
- props . send ( {
350
- type : 'selectOperation' ,
351
- data : {
352
- targetSourceRange : sourceRangeFromRust ( props . item . sourceRange ) ,
353
- } ,
354
- } )
355
380
}
356
381
357
382
/**
@@ -432,6 +457,20 @@ const OperationItem = (props: {
432
457
}
433
458
}
434
459
460
+ function startSketchOnOffsetPlane ( ) {
461
+ if ( isOffsetPlane ( props . item ) ) {
462
+ const artifact = findOperationArtifact ( props . item )
463
+ if ( artifact ?. id ) {
464
+ sceneInfra . modelingSend ( {
465
+ type : 'Enter sketch' ,
466
+ data : { forceNewSketch : true } ,
467
+ } )
468
+
469
+ void selectOffsetSketchPlane ( artifact )
470
+ }
471
+ }
472
+ }
473
+
435
474
const menuItems = useMemo (
436
475
( ) => [
437
476
< ContextMenuItem
@@ -477,6 +516,13 @@ const OperationItem = (props: {
477
516
</ ContextMenuItem > ,
478
517
]
479
518
: [ ] ) ,
519
+ ...( isOffsetPlane ( props . item )
520
+ ? [
521
+ < ContextMenuItem onClick = { startSketchOnOffsetPlane } >
522
+ Start Sketch
523
+ </ ContextMenuItem > ,
524
+ ]
525
+ : [ ] ) ,
480
526
...( props . item . type === 'StdLibCall' ||
481
527
props . item . type === 'VariableDeclaration'
482
528
? [
@@ -550,22 +596,63 @@ const OperationItem = (props: {
550
596
[ props . item , props . send ]
551
597
)
552
598
599
+ const enabled = ! props . sketchNoFace || isOffsetPlane ( props . item )
600
+
553
601
return (
554
602
< OperationItemWrapper
603
+ selectable = { enabled }
555
604
icon = { getOperationIcon ( props . item ) }
556
605
name = { name }
557
606
variableName = { variableName }
558
607
valueDetail = { valueDetail }
559
608
menuItems = { menuItems }
560
609
onClick = { selectOperation }
561
- onDoubleClick = { enterEditFlow }
610
+ onDoubleClick = { props . sketchNoFace ? undefined : enterEditFlow } // no double click in "Sketch no face" mode
562
611
errors = { errors }
612
+ greyedOut = { ! enabled }
563
613
/>
564
614
)
565
615
}
566
616
567
617
const DefaultPlanes = ( ) => {
568
618
const { state : modelingState , send } = useModelingContext ( )
619
+ const sketchNoFace = modelingState . matches ( 'Sketch no face' )
620
+
621
+ const onClickPlane = useCallback (
622
+ ( planeId : string ) => {
623
+ if ( sketchNoFace ) {
624
+ selectDefaultSketchPlane ( planeId )
625
+ } else {
626
+ const foundDefaultPlane =
627
+ rustContext . defaultPlanes !== null &&
628
+ Object . entries ( rustContext . defaultPlanes ) . find (
629
+ ( [ , plane ] ) => plane === planeId
630
+ )
631
+ if ( foundDefaultPlane ) {
632
+ send ( {
633
+ type : 'Set selection' ,
634
+ data : {
635
+ selectionType : 'defaultPlaneSelection' ,
636
+ selection : {
637
+ name : foundDefaultPlane [ 0 ] as DefaultPlaneStr ,
638
+ id : planeId ,
639
+ } ,
640
+ } ,
641
+ } )
642
+ }
643
+ }
644
+ } ,
645
+ [ sketchNoFace ]
646
+ )
647
+
648
+ const startSketchOnDefaultPlane = useCallback ( ( planeId : string ) => {
649
+ sceneInfra . modelingSend ( {
650
+ type : 'Enter sketch' ,
651
+ data : { forceNewSketch : true } ,
652
+ } )
653
+
654
+ selectDefaultSketchPlane ( planeId )
655
+ } , [ ] )
569
656
570
657
const defaultPlanes = rustContext . defaultPlanes
571
658
if ( ! defaultPlanes ) return null
@@ -603,7 +690,15 @@ const DefaultPlanes = () => {
603
690
customSuffix = { plane . customSuffix }
604
691
icon = { 'plane' }
605
692
name = { plane . name }
606
- selectable = { false }
693
+ selectable = { true }
694
+ onClick = { ( ) => onClickPlane ( plane . id ) }
695
+ menuItems = { [
696
+ < ContextMenuItem
697
+ onClick = { ( ) => startSketchOnDefaultPlane ( plane . id ) }
698
+ >
699
+ Start Sketch
700
+ </ ContextMenuItem > ,
701
+ ] }
607
702
visibilityToggle = { {
608
703
visible : modelingState . context . defaultPlaneVisibility [ plane . key ] ,
609
704
onVisibilityChange : ( ) => {
@@ -620,3 +715,17 @@ const DefaultPlanes = () => {
620
715
</ div >
621
716
)
622
717
}
718
+
719
+ type StdLibCallOp = Extract < Operation , { type : 'StdLibCall' } >
720
+
721
+ const isOffsetPlane = ( item : Operation ) : item is StdLibCallOp => {
722
+ return item . type === 'StdLibCall' && item . name === 'offsetPlane'
723
+ }
724
+
725
+ const findOperationArtifact = ( item : StdLibCallOp ) => {
726
+ const nodePath = JSON . stringify ( item . nodePath )
727
+ const artifact = [ ...kclManager . artifactGraph . values ( ) ] . find (
728
+ ( a ) => JSON . stringify ( ( a as Plane ) . codeRef ?. nodePath ) === nodePath
729
+ )
730
+ return artifact
731
+ }
0 commit comments