Compare commits

...

6 Commits

Author SHA1 Message Date
1953ca0607 WIP: Fix setUpDraftSegment to work without a variable
This isn't finished.  It needs to look up the SketchGroup coordinates
without a variable.
2024-08-08 15:49:15 -04:00
cb9fa71645 Fix modifyAst() to work with top-level expression statements 2024-08-08 15:49:15 -04:00
a7f0a5607d WIP: Enable Edit Sketch without variable declaration 2024-08-08 15:49:15 -04:00
eb28ed6cbf WIP: Add saving artifact from click and getting plane 2024-08-08 15:49:15 -04:00
3c84ef8592 Add id to all artifact types 2024-08-08 15:49:15 -04:00
9b966de7f0 Add ArtifactId type 2024-08-08 15:49:15 -04:00
6 changed files with 223 additions and 57 deletions

View File

@ -84,7 +84,11 @@ import {
createPipeSubstitution,
findUniqueName,
} from 'lang/modifyAst'
import { Selections, getEventForSegmentSelection } from 'lib/selections'
import {
Selection,
Selections,
getEventForSegmentSelection,
} from 'lib/selections'
import { getTangentPointFromPreviousArc } from 'lib/utils2d'
import { createGridHelper, orthoScale, perspScale } from './helpers'
import { Models } from '@kittycad/lib'
@ -98,6 +102,11 @@ import {
import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import {
ArtifactGraph,
ArtifactId,
getPlaneOrFaceFromSelection,
} from 'lang/std/artifactGraph'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -582,15 +591,20 @@ export class SceneEntities {
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
['VariableDeclaration', 'ExpressionStatement']
) as { node: { type: string } } | Error
if (trap(_node1)) return Promise.reject(_node1)
const variableDeclarationName =
_node1.node?.declarations?.[0]?.id?.name || ''
const variableDeclarationName = (_node1.node.type === 'VariableDeclaration') ?
(_node1.node as VariableDeclaration).declarations[0]?.id?.name || '' :
''
const sg = kclManager.programMemory.get(
const sgMemItem = kclManager.programMemory.get(
variableDeclarationName
) as SketchGroup
)
if (sgMemItem?.type !== 'SketchGroup') {
return Promise.reject(new Error('SketchGroup not found in programMemory'))
}
const sg: SketchGroup = sgMemItem
const lastSeg = sg.value.slice(-1)[0] || sg.start
const index = sg.value.length // because we've added a new segment that's not in the memory yet, no need for `-1`
@ -1762,6 +1776,33 @@ export function getParentGroup(
return null
}
export async function planeOrFaceFromSelection({
artifactGraph,
selection,
}: {
artifactGraph: ArtifactGraph
selection: Selection
}): Promise<{
id: ArtifactId
faceDetails: Models['GetSketchModePlane_type']
} | null> {
// If the selection doesn't have an artifactId associated with it, we can't
// do it.
if (!selection.artifactId) return null
const planeOrFace = getPlaneOrFaceFromSelection(
selection.artifactId,
artifactGraph
)
if (!planeOrFace) return null
if (planeOrFace?.type === 'plane') {
const faceDetails = await getFaceDetails(planeOrFace.id)
return { id: planeOrFace.id, faceDetails }
}
// TODO: Handle wall or cap artifact.
return null
}
export function sketchGroupFromPathToNode({
pathToNode,
ast,
@ -1827,11 +1868,51 @@ export function getSketchQuaternion(
return getQuaternionFromZAxis(massageFormats(zAxis))
}
export async function getSketchOrientationDetails(
artifactGraph: ArtifactGraph,
selection: Selection,
sketchPathToNode: PathToNode
): Promise<{
quat: Quaternion
sketchDetails: SketchDetails & { faceId?: string }
sketchDetails: {
zAxis: [number, number, number]
yAxis: [number, number, number]
origin: [number, number, number]
faceId: string
}
}> {
const plane = await planeOrFaceFromSelection({
artifactGraph,
selection,
})
if (plane) {
const details = plane.faceDetails
console.warn('Found plane', plane)
const zAxis: [number, number, number] = [
details.z_axis.x,
details.z_axis.y,
details.z_axis.z,
]
const yAxis: [number, number, number] = [
details.y_axis.x,
details.y_axis.y,
details.y_axis.z,
]
const origin: [number, number, number] = [
details.origin.x,
details.origin.y,
details.origin.z,
]
return {
sketchDetails: {
zAxis,
yAxis,
origin,
faceId: plane.id,
},
}
}
// We couldn't find the plane or face, so try to look at the AST, and find it
// through there.
const sketchGroup = sketchGroupFromPathToNode({
pathToNode: sketchPathToNode,
ast: kclManager.ast,
@ -1843,9 +1924,7 @@ export async function getSketchOrientationDetails(
if (sketchGroup.on.type === 'plane') {
const zAxis = sketchGroup?.on.zAxis
return {
quat: getQuaternionFromZAxis(massageFormats(zAxis)),
sketchDetails: {
sketchPathToNode,
zAxis: [zAxis.x, zAxis.y, zAxis.z],
yAxis: [
sketchGroup.on.yAxis.x,
@ -1864,14 +1943,8 @@ export async function getSketchOrientationDetails(
if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
return Promise.reject('face info')
const { z_axis, y_axis, origin } = faceInfo
const quaternion = quaternionFromUpNForward(
new Vector3(y_axis.x, y_axis.y, y_axis.z),
new Vector3(z_axis.x, z_axis.y, z_axis.z)
)
return {
quat: quaternion,
sketchDetails: {
sketchPathToNode,
zAxis: [z_axis.x, z_axis.y, z_axis.z],
yAxis: [y_axis.x, y_axis.y, y_axis.z],
origin: [origin.x, origin.y, origin.z],

View File

@ -589,12 +589,17 @@ export const ModelingMachineProvider = ({
}
},
'animate-to-sketch': async ({ selectionRanges }) => {
const sourceRange = selectionRanges.codeBasedSelections[0].range
const selection = selectionRanges.codeBasedSelections[0]
const sourceRange = selection.range
const sketchPathToNode = getNodePathFromSourceRange(
kclManager.ast,
sourceRange
)
const info = await getSketchOrientationDetails(sketchPathToNode || [])
const info = await getSketchOrientationDetails(
engineCommandManager.artifactGraph,
selection,
sketchPathToNode || []
)
await letEngineAnimateAndSyncCamAfter(
engineCommandManager,
info?.sketchDetails?.faceId || ''

View File

@ -18,6 +18,7 @@ import {
ProgramMemory,
SourceRange,
SketchGroup,
ExpressionStatement,
} from './wasm'
import {
isNodeSafeToReplacePath,
@ -82,15 +83,22 @@ export function addStartProfileAt(
const _node1 = getNodeFromPath<VariableDeclaration>(
node,
pathToNode,
'VariableDeclaration'
)
['VariableDeclaration', 'ExpressionStatement']
) as { node: { type: string } } | Error
if (err(_node1)) return _node1
const variableDeclaration = _node1.node
if (variableDeclaration.type !== 'VariableDeclaration') {
return new Error('variableDeclaration.init.type !== PipeExpression')
}
const _node = { ...node }
const init = variableDeclaration.declarations[0].init
let expr: Value
let variableDeclaration: VariableDeclaration | undefined
if (_node1.node.type === 'VariableDeclaration') {
const node: VariableDeclaration = _node1.node as VariableDeclaration
variableDeclaration = node
expr = node.declarations[0].init
} else if (_node1.node.type === 'ExpressionStatement') {
const node: ExpressionStatement = _node1.node as ExpressionStatement
expr = node.expression
} else {
return new Error(`Unrecognized node type ${_node1.node.type}`)
}
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
createArrayExpression([
createLiteral(roundOff(at[0])),
@ -98,11 +106,11 @@ export function addStartProfileAt(
]),
createPipeSubstitution(),
])
if (init.type === 'PipeExpression') {
init.body.splice(1, 0, startProfileAt)
} else {
if (expr.type === 'PipeExpression') {
expr.body.splice(1, 0, startProfileAt)
} else if (variableDeclaration) {
variableDeclaration.declarations[0].init = createPipeExpression([
init,
expr,
startProfileAt,
])
}

View File

@ -791,7 +791,7 @@ export function isSingleCursorInPipe(
const pathToNode = getNodePathFromSourceRange(ast, selection.range)
const nodeTypes = pathToNode.map(([, type]) => type)
if (nodeTypes.includes('FunctionExpression')) return false
if (!nodeTypes.includes('VariableDeclaration')) return false
// if (!nodeTypes.includes('VariableDeclaration')) return false
if (nodeTypes.includes('PipeExpression')) return true
return false
}

View File

@ -3,6 +3,8 @@ import { Models } from '@kittycad/lib'
import { getNodePathFromSourceRange } from 'lang/queryAst'
import { err } from 'lib/trap'
export type ArtifactId = string
interface CommonCommandProperties {
range: SourceRange
pathToNode: PathToNode
@ -10,17 +12,20 @@ interface CommonCommandProperties {
export interface PlaneArtifact {
type: 'plane'
id: ArtifactId
pathIds: Array<string>
codeRef: CommonCommandProperties
}
export interface PlaneArtifactRich {
type: 'plane'
id: ArtifactId
paths: Array<PathArtifact>
codeRef: CommonCommandProperties
}
export interface PathArtifact {
type: 'path'
id: ArtifactId
planeId: string
segIds: Array<string>
extrusionId: string
@ -30,10 +35,12 @@ export interface PathArtifact {
interface solid2D {
type: 'solid2D'
id: ArtifactId
pathId: string
}
export interface PathArtifactRich {
type: 'path'
id: ArtifactId
plane: PlaneArtifact | WallArtifact
segments: Array<SegmentArtifact>
extrusion: ExtrusionArtifact
@ -42,6 +49,7 @@ export interface PathArtifactRich {
interface SegmentArtifact {
type: 'segment'
id: ArtifactId
pathId: string
surfaceId: string
edgeIds: Array<string>
@ -50,6 +58,7 @@ interface SegmentArtifact {
}
interface SegmentArtifactRich {
type: 'segment'
id: ArtifactId
path: PathArtifact
surf: WallArtifact
edges: Array<ExtrudeEdge>
@ -59,6 +68,7 @@ interface SegmentArtifactRich {
interface ExtrusionArtifact {
type: 'extrusion'
id: ArtifactId
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
@ -66,6 +76,7 @@ interface ExtrusionArtifact {
}
interface ExtrusionArtifactRich {
type: 'extrusion'
id: ArtifactId
path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact>
edges: Array<ExtrudeEdge>
@ -74,6 +85,7 @@ interface ExtrusionArtifactRich {
interface WallArtifact {
type: 'wall'
id: ArtifactId
segId: string
edgeCutEdgeIds: Array<string>
extrusionId: string
@ -81,6 +93,7 @@ interface WallArtifact {
}
interface CapArtifact {
type: 'cap'
id: ArtifactId
subType: 'start' | 'end'
edgeCutEdgeIds: Array<string>
extrusionId: string
@ -89,6 +102,7 @@ interface CapArtifact {
interface ExtrudeEdge {
type: 'extrudeEdge'
id: ArtifactId
segId: string
extrusionId: string
edgeId: string
@ -97,6 +111,7 @@ interface ExtrudeEdge {
/** A edgeCut is a more generic term for both fillet or chamfer */
interface EdgeCut {
type: 'edgeCut'
id: ArtifactId
subType: 'fillet' | 'chamfer'
consumedEdgeId: string
edgeIds: Array<string>
@ -106,6 +121,7 @@ interface EdgeCut {
interface EdgeCutEdge {
type: 'edgeCutEdge'
id: ArtifactId
edgeCutId: string
surfaceId: string
}
@ -122,7 +138,7 @@ export type Artifact =
| EdgeCutEdge
| solid2D
export type ArtifactGraph = Map<string, Artifact>
export type ArtifactGraph = Map<ArtifactId, Artifact>
export type EngineCommand = Models['WebSocketRequest_type']
@ -149,7 +165,7 @@ export function createArtifactGraph({
responseMap: ResponseMap
ast: Program
}) {
const myMap = new Map<string, Artifact>()
const myMap = new Map<ArtifactId, Artifact>()
/** see docstring for {@link getArtifactsToUpdate} as to why this is needed */
let currentPlaneId = ''
@ -166,7 +182,7 @@ export function createArtifactGraph({
const artifactsToUpdate = getArtifactsToUpdate({
orderedCommand,
responseMap,
getArtifact: (id: string) => myMap.get(id),
getArtifact: (id: ArtifactId) => myMap.get(id),
currentPlaneId,
ast,
})
@ -210,7 +226,7 @@ function mergeArtifacts(
* It does not mutate the map directly, but returns an array of artifacts to update
*
* @param currentPlaneId is only needed for `start_path` commands because this command does not have a pathId
* instead it relies on the id used with the `enable_sketch_mode` command, so this much be kept track of
* instead it relies on the id used with the `enable_sketch_mode` command, so this must be kept track of
* outside of this function. It would be good to update the `start_path` command to include the planeId so we
* can remove this.
*/
@ -224,11 +240,11 @@ export function getArtifactsToUpdate({
orderedCommand: OrderedCommand
responseMap: ResponseMap
/** Passing in a getter because we don't wan this function to update the map directly */
getArtifact: (id: string) => Artifact | undefined
getArtifact: (id: ArtifactId) => Artifact | undefined
currentPlaneId: string
ast: Program
}): Array<{
id: string
id: ArtifactId
artifact: Artifact
}> {
const pathToNode = getNodePathFromSourceRange(ast, range)
@ -253,6 +269,7 @@ export function getArtifactsToUpdate({
id: currentPlaneId,
artifact: {
type: 'wall',
id: currentPlaneId,
segId: existingPlane.segId,
edgeCutEdgeIds: existingPlane.edgeCutEdgeIds,
extrusionId: existingPlane.extrusionId,
@ -262,7 +279,10 @@ export function getArtifactsToUpdate({
]
} else {
return [
{ id: currentPlaneId, artifact: { type: 'plane', pathIds, codeRef } },
{
id: currentPlaneId,
artifact: { type: 'plane', id: currentPlaneId, pathIds, codeRef },
},
]
}
} else if (cmd.type === 'start_path') {
@ -270,6 +290,7 @@ export function getArtifactsToUpdate({
id,
artifact: {
type: 'path',
id,
segIds: [],
planeId: currentPlaneId,
extrusionId: '',
@ -282,7 +303,7 @@ export function getArtifactsToUpdate({
if (plane?.type === 'plane') {
returnArr.push({
id: currentPlaneId,
artifact: { type: 'plane', pathIds: [id], codeRef },
artifact: { type: 'plane', id: currentPlaneId, pathIds: [id], codeRef },
})
}
if (plane?.type === 'wall') {
@ -290,6 +311,7 @@ export function getArtifactsToUpdate({
id: currentPlaneId,
artifact: {
type: 'wall',
id: currentPlaneId,
segId: plane.segId,
edgeCutEdgeIds: plane.edgeCutEdgeIds,
extrusionId: plane.extrusionId,
@ -304,6 +326,7 @@ export function getArtifactsToUpdate({
id,
artifact: {
type: 'segment',
id,
pathId,
surfaceId: '',
edgeIds: [],
@ -313,21 +336,22 @@ export function getArtifactsToUpdate({
const path = getArtifact(pathId)
if (path?.type === 'path')
returnArr.push({
id: pathId,
id: path.id,
artifact: { ...path, segIds: [id] },
})
if (
response?.type === 'modeling' &&
response.data.modeling_response.type === 'close_path'
) {
const id = response.data.modeling_response.data.face_id
returnArr.push({
id: response.data.modeling_response.data.face_id,
artifact: { type: 'solid2D', pathId },
id,
artifact: { type: 'solid2D', id, pathId },
})
const path = getArtifact(pathId)
if (path?.type === 'path')
returnArr.push({
id: pathId,
id: path.id,
artifact: {
...path,
solid2dId: response.data.modeling_response.data.face_id,
@ -340,6 +364,7 @@ export function getArtifactsToUpdate({
id,
artifact: {
type: 'extrusion',
id,
pathId: cmd.target,
surfaceIds: [],
edgeIds: [],
@ -349,7 +374,7 @@ export function getArtifactsToUpdate({
const path = getArtifact(cmd.target)
if (path?.type === 'path')
returnArr.push({
id: cmd.target,
id: path.id,
artifact: { ...path, extrusionId: id },
})
return returnArr
@ -371,6 +396,7 @@ export function getArtifactsToUpdate({
id: face_id,
artifact: {
type: 'wall',
id: face_id,
segId: curve_id,
edgeCutEdgeIds: [],
extrusionId: path.extrusionId,
@ -378,7 +404,7 @@ export function getArtifactsToUpdate({
},
})
returnArr.push({
id: curve_id,
id: seg.id,
artifact: { ...seg, surfaceId: face_id },
})
const extrusion = getArtifact(path.extrusionId)
@ -403,6 +429,7 @@ export function getArtifactsToUpdate({
id: face_id,
artifact: {
type: 'cap',
id: face_id,
subType: cap === 'bottom' ? 'start' : 'end',
edgeCutEdgeIds: [],
extrusionId: path.extrusionId,
@ -412,7 +439,7 @@ export function getArtifactsToUpdate({
const extrusion = getArtifact(path.extrusionId)
if (extrusion?.type !== 'extrusion') return
returnArr.push({
id: path.extrusionId,
id: extrusion.id,
artifact: {
...extrusion,
surfaceIds: [face_id],
@ -427,6 +454,7 @@ export function getArtifactsToUpdate({
id,
artifact: {
type: 'edgeCut',
id,
subType: cmd.cut_type,
consumedEdgeId: cmd.edge_id,
edgeIds: [],
@ -437,7 +465,7 @@ export function getArtifactsToUpdate({
const consumedEdge = getArtifact(cmd.edge_id)
if (consumedEdge?.type === 'segment') {
returnArr.push({
id: cmd.edge_id,
id: consumedEdge.id,
artifact: { ...consumedEdge, edgeCutId: id },
})
}
@ -464,7 +492,7 @@ export function filterArtifacts<T extends Artifact['type'][]>(
(!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>))
)
) as Map<string, Extract<Artifact, { type: T[number] }>>
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
}
export function getArtifactsOfTypes<T extends Artifact['type'][]>(
@ -478,7 +506,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
predicate?: (value: Extract<Artifact, { type: T[number] }>) => boolean
},
map: ArtifactGraph
): Map<string, Extract<Artifact, { type: T[number] }>> {
): Map<ArtifactId, Extract<Artifact, { type: T[number] }>> {
return new Map(
[...map].filter(
([key, value]) =>
@ -487,7 +515,7 @@ export function getArtifactsOfTypes<T extends Artifact['type'][]>(
(!predicate ||
predicate(value as Extract<Artifact, { type: T[number] }>))
)
) as Map<string, Extract<Artifact, { type: T[number] }>>
) as Map<ArtifactId, Extract<Artifact, { type: T[number] }>>
}
export function getArtifactOfTypes<T extends Artifact['type'][]>(
@ -495,7 +523,7 @@ export function getArtifactOfTypes<T extends Artifact['type'][]>(
key,
types,
}: {
key: string
key: ArtifactId
types: T
},
map: ArtifactGraph
@ -517,6 +545,7 @@ export function expandPlane(
)
return {
type: 'plane',
id: plane.id,
paths: Array.from(paths.values()),
codeRef: plane.codeRef,
}
@ -545,6 +574,7 @@ export function expandPath(
if (err(plane)) return plane
return {
type: 'path',
id: path.id,
segments: Array.from(segs.values()),
extrusion,
plane,
@ -571,6 +601,7 @@ export function expandExtrusion(
if (err(path)) return path
return {
type: 'extrusion',
id: extrusion.id,
surfaces: Array.from(surfs.values()),
edges: Array.from(edges.values()),
path,
@ -606,6 +637,7 @@ export function expandSegment(
return {
type: 'segment',
id: segment.id,
path,
surf,
edges: Array.from(edges.values()),
@ -656,7 +688,7 @@ export function getWallCodeRef(
}
export function getExtrusionFromSuspectedExtrudeSurface(
id: string,
id: ArtifactId,
artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error {
const artifact = getArtifactOfTypes(
@ -671,7 +703,7 @@ export function getExtrusionFromSuspectedExtrudeSurface(
}
export function getExtrusionFromSuspectedPath(
id: string,
id: ArtifactId,
artifactGraph: ArtifactGraph
): ExtrusionArtifact | Error {
const path = getArtifactOfTypes({ key: id, types: ['path'] }, artifactGraph)
@ -681,3 +713,33 @@ export function getExtrusionFromSuspectedPath(
artifactGraph
)
}
/**
* Get the plane or face from a selection.
*
* TODO: Handle sketch on face.
*/
export function getPlaneOrFaceFromSelection(
id: ArtifactId,
artifactGraph: ArtifactGraph
): PlaneArtifactRich | null {
const selection = artifactGraph.get(id)
if (!selection) return null
if (selection.type === 'solid2D') {
const path = artifactGraph.get(selection.pathId)
if (path?.type !== 'path') return null
const plane = artifactGraph.get(path.planeId)
if (plane?.type !== 'plane') return null
return expandPlane(plane, artifactGraph)
} else if (selection.type === 'wall' || selection.type === 'cap') {
const extrusion = artifactGraph.get(selection.extrusionId)
if (extrusion?.type !== 'extrusion') return null
const path = artifactGraph.get(extrusion.pathId)
if (path?.type !== 'path') return null
const plane = artifactGraph.get(path.planeId)
// TODO: For sketch on face, this won't be a plane.
if (plane?.type !== 'plane') return null
return expandPlane(plane, artifactGraph)
}
return null
}

View File

@ -30,6 +30,7 @@ import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
import { PathToNodeMap } from 'lang/std/sketchcombos'
import { err } from 'lib/trap'
import {
ArtifactId,
getArtifactOfTypes,
getArtifactsOfTypes,
getCapCodeRef,
@ -56,6 +57,7 @@ export type Selection = {
| 'line'
| 'arc'
| 'all'
artifactId?: ArtifactId
range: SourceRange
}
export type Selections = {
@ -100,7 +102,11 @@ export async function getEventForSelectWithPoint({
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'solid2D' },
selection: {
artifactId: data.entity_id,
range: codeRef.range,
type: 'solid2D',
},
},
}
}
@ -112,6 +118,7 @@ export async function getEventForSelectWithPoint({
data: {
selectionType: 'singleCodeCursor',
selection: {
artifactId: data.entity_id,
range: codeRef.range,
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
},
@ -128,7 +135,11 @@ export async function getEventForSelectWithPoint({
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: codeRef.range, type: 'extrude-wall' },
selection: {
artifactId: data.entity_id,
range: codeRef.range,
type: 'extrude-wall',
},
},
}
}
@ -137,7 +148,11 @@ export async function getEventForSelectWithPoint({
type: 'Set selection',
data: {
selectionType: 'singleCodeCursor',
selection: { range: _artifact.codeRef.range, type: 'default' },
selection: {
artifactId: data.entity_id,
range: _artifact.codeRef.range,
type: 'default',
},
},
}
}
@ -642,9 +657,12 @@ export function updateSelections(
const nodeMeta = getNodeFromPath<Value>(ast, pathToNode)
if (err(nodeMeta)) return undefined
const node = nodeMeta.node
const prevCodeBasedSelection =
prevSelectionRanges.codeBasedSelections[Number(index)]
return {
artifactId: prevCodeBasedSelection?.artifactId,
range: [node.start, node.end],
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
type: prevCodeBasedSelection?.type,
}
})
.filter((x?: Selection) => x !== undefined) as Selection[]