refactor codeToIdSelections
(#5432)
* add test for original range to artifact conversion * try naieve refactor * types clean up * typo * break function into smaller functions * optimizations * better comments * camera test tweak * fmt * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * overflow fix * update binary search to ignore end ranges * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * break binary search into sub function --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -11,6 +11,7 @@ import {
|
||||
Expr,
|
||||
defaultSourceRange,
|
||||
topLevelRange,
|
||||
ArtifactGraph,
|
||||
} from 'lang/wasm'
|
||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||
import { isNonNullable, uuidv4 } from 'lib/utils'
|
||||
@ -31,19 +32,13 @@ import { PathToNodeMap } from 'lang/std/sketchcombos'
|
||||
import { err } from 'lib/trap'
|
||||
import {
|
||||
Artifact,
|
||||
getArtifactOfTypes,
|
||||
getArtifactsOfTypes,
|
||||
getCapCodeRef,
|
||||
getSweepEdgeCodeRef,
|
||||
getSolid2dCodeRef,
|
||||
getWallCodeRef,
|
||||
CodeRef,
|
||||
getCodeRefsByArtifactId,
|
||||
ArtifactId,
|
||||
getFaceCodeRef,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { DefaultPlaneStr } from './planes'
|
||||
import { ArtifactEntry, ArtifactIndex } from './artifactIndex'
|
||||
|
||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
@ -54,38 +49,7 @@ export type DefaultPlaneSelection = {
|
||||
id: string
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link Artifact} instead. */
|
||||
type Selection__old =
|
||||
| {
|
||||
type:
|
||||
| 'default'
|
||||
| 'line-end'
|
||||
| 'line-mid'
|
||||
| 'extrude-wall'
|
||||
| 'solid2d'
|
||||
| 'start-cap'
|
||||
| 'end-cap'
|
||||
| 'point'
|
||||
| 'edge'
|
||||
| 'adjacent-edge'
|
||||
| 'line'
|
||||
| 'arc'
|
||||
| 'all'
|
||||
range: SourceRange
|
||||
}
|
||||
| {
|
||||
type: 'opposite-edgeCut' | 'adjacent-edgeCut' | 'base-edgeCut'
|
||||
range: SourceRange
|
||||
// TODO this is a temporary measure that well be made redundant with: https://github.com/KittyCAD/modeling-app/pull/3836
|
||||
secondaryRange: SourceRange
|
||||
}
|
||||
export type NonCodeSelection = Axis | DefaultPlaneSelection
|
||||
|
||||
/** @deprecated Use {@link Selection} instead. */
|
||||
export type Selections__old = {
|
||||
otherSelections: NonCodeSelection[]
|
||||
codeBasedSelections: Selection__old[]
|
||||
}
|
||||
export interface Selection {
|
||||
artifact?: Artifact
|
||||
codeRef: CodeRef
|
||||
@ -95,76 +59,6 @@ export type Selections = {
|
||||
graphSelections: Array<Selection>
|
||||
}
|
||||
|
||||
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
|
||||
* this function should only be used for backwards compatibility with old functions.
|
||||
*/
|
||||
function convertSelectionToOld(selection: Selection): Selection__old | null {
|
||||
// return {} as Selection__old
|
||||
// TODO implementation
|
||||
const _artifact = selection.artifact
|
||||
if (_artifact?.type === 'solid2d') {
|
||||
const codeRef = getSolid2dCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return { range: codeRef.range, type: 'solid2d' }
|
||||
}
|
||||
if (_artifact?.type === 'cap') {
|
||||
const codeRef = getCapCodeRef(_artifact, engineCommandManager.artifactGraph)
|
||||
if (err(codeRef)) return null
|
||||
return {
|
||||
range: codeRef.range,
|
||||
type: _artifact?.subType === 'end' ? 'end-cap' : 'start-cap',
|
||||
}
|
||||
}
|
||||
if (_artifact?.type === 'wall') {
|
||||
const codeRef = getWallCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
return { range: codeRef.range, type: 'extrude-wall' }
|
||||
}
|
||||
if (_artifact?.type === 'segment' || _artifact?.type === 'path') {
|
||||
return { range: _artifact.codeRef.range, type: 'default' }
|
||||
}
|
||||
if (_artifact?.type === 'sweepEdge') {
|
||||
const codeRef = getSweepEdgeCodeRef(
|
||||
_artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(codeRef)) return null
|
||||
if (_artifact?.subType === 'adjacent') {
|
||||
return { range: codeRef.range, type: 'adjacent-edge' }
|
||||
}
|
||||
return { range: codeRef.range, type: 'edge' }
|
||||
}
|
||||
if (_artifact?.type === 'edgeCut') {
|
||||
const codeRef = _artifact.codeRef
|
||||
return { range: codeRef.range, type: 'default' }
|
||||
}
|
||||
if (selection?.codeRef?.range) {
|
||||
return { range: selection.codeRef.range, type: 'default' }
|
||||
}
|
||||
return null
|
||||
}
|
||||
/** @deprecated If you're writing a new function, it should use {@link Selection} and not {@link Selection__old}
|
||||
* this function should only be used for backwards compatibility with old functions.
|
||||
*/
|
||||
export function convertSelectionsToOld(selection: Selections): Selections__old {
|
||||
const selections: Selection__old[] = []
|
||||
for (const artifact of selection.graphSelections) {
|
||||
const converted = convertSelectionToOld(artifact)
|
||||
if (converted) selections.push(converted)
|
||||
}
|
||||
const selectionsOld: Selections__old = {
|
||||
otherSelections: selection.otherSelections,
|
||||
codeBasedSelections: selections,
|
||||
}
|
||||
return selectionsOld
|
||||
}
|
||||
|
||||
export async function getEventForSelectWithPoint({
|
||||
data,
|
||||
}: Extract<
|
||||
@ -310,7 +204,6 @@ export function handleSelectionBatch({
|
||||
selections.graphSelections.forEach(({ artifact }) => {
|
||||
artifact?.id &&
|
||||
selectionToEngine.push({
|
||||
type: 'default',
|
||||
id: artifact?.id,
|
||||
range:
|
||||
getCodeRefsByArtifactId(
|
||||
@ -350,7 +243,6 @@ export function handleSelectionBatch({
|
||||
}
|
||||
|
||||
type SelectionToEngine = {
|
||||
type: Selection__old['type']
|
||||
id?: string
|
||||
range: SourceRange
|
||||
}
|
||||
@ -360,11 +252,13 @@ export function processCodeMirrorRanges({
|
||||
selectionRanges,
|
||||
isShiftDown,
|
||||
ast,
|
||||
artifactGraph,
|
||||
}: {
|
||||
codeMirrorRanges: readonly SelectionRange[]
|
||||
selectionRanges: Selections
|
||||
isShiftDown: boolean
|
||||
ast: Program
|
||||
artifactGraph: ArtifactGraph
|
||||
}): null | {
|
||||
modelingEvent: ModelingMachineEvent
|
||||
engineEvents: Models['WebSocketRequest_type'][]
|
||||
@ -392,8 +286,11 @@ export function processCodeMirrorRanges({
|
||||
},
|
||||
}
|
||||
})
|
||||
const idBasedSelections: SelectionToEngine[] =
|
||||
codeToIdSelections(codeBasedSelections)
|
||||
const idBasedSelections: SelectionToEngine[] = codeToIdSelections(
|
||||
codeBasedSelections,
|
||||
artifactGraph,
|
||||
engineCommandManager.artifactIndex
|
||||
)
|
||||
const selections: Selection[] = []
|
||||
for (const { id, range } of idBasedSelections) {
|
||||
if (!id) {
|
||||
@ -406,11 +303,8 @@ export function processCodeMirrorRanges({
|
||||
})
|
||||
continue
|
||||
}
|
||||
const artifact = engineCommandManager.artifactGraph.get(id)
|
||||
const codeRefs = getCodeRefsByArtifactId(
|
||||
id,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const artifact = artifactGraph.get(id)
|
||||
const codeRefs = getCodeRefsByArtifactId(id, artifactGraph)
|
||||
if (artifact && codeRefs) {
|
||||
selections.push({ artifact, codeRef: codeRefs[0] })
|
||||
} else if (codeRefs) {
|
||||
@ -601,234 +495,150 @@ export function canSubmitSelectionArg(
|
||||
)
|
||||
}
|
||||
|
||||
export function codeToIdSelections(
|
||||
selections: Selection[]
|
||||
): SelectionToEngine[] {
|
||||
const selectionsOld = convertSelectionsToOld({
|
||||
graphSelections: selections,
|
||||
otherSelections: [],
|
||||
}).codeBasedSelections
|
||||
return selectionsOld
|
||||
.flatMap((selection): null | SelectionToEngine[] => {
|
||||
const { type } = selection
|
||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||
const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
|
||||
.map(([id, artifact]) => {
|
||||
const codeRef = getFaceCodeRef(artifact)
|
||||
if (!codeRef) return null
|
||||
return isOverlap(codeRef.range, selection.range)
|
||||
? {
|
||||
artifact,
|
||||
selection,
|
||||
id,
|
||||
}
|
||||
: null
|
||||
})
|
||||
.filter(isNonNullable)
|
||||
/**
|
||||
* Find the index of the last range where range[0] < targetStart
|
||||
* This is used as a starting point for linear search of overlapping ranges
|
||||
* @param index The sorted array of ranges to search through
|
||||
* @param targetStart The start position to compare against
|
||||
* @returns The index of the last range where range[0] < targetStart
|
||||
*/
|
||||
export function findLastRangeStartingBefore(
|
||||
index: ArtifactIndex,
|
||||
targetStart: number
|
||||
): number {
|
||||
let left = 0
|
||||
let right = index.length - 1
|
||||
let lastValidIndex = 0
|
||||
|
||||
/** TODO refactor
|
||||
* selections in our app is a sourceRange plus some metadata
|
||||
* The metadata is just a union type string of different types of artifacts or 3d features 'extrude-wall' 'segment' etc
|
||||
* Because the source range is not enough to figure out what the user selected, so here we're using filtering through all the artifacts
|
||||
* to find something that matches both the source range and the metadata.
|
||||
*
|
||||
* What we should migrate to is just storing what the user selected by what it matched in the artifactGraph it will simply the below a lot.
|
||||
*
|
||||
* In the case of a user moving the cursor them, we will still need to figure out what artifact from the graph matches best, but we will just need sane defaults
|
||||
* and most of the time we can expect the user to be clicking in the 3d scene instead.
|
||||
*/
|
||||
let bestCandidate:
|
||||
| {
|
||||
id: ArtifactId
|
||||
artifact: unknown
|
||||
selection: Selection__old
|
||||
}
|
||||
| undefined
|
||||
overlappingEntries.forEach((entry) => {
|
||||
// TODO probably need to remove much of the `type === 'xyz'` below
|
||||
if (type === 'default' && entry.artifact.type === 'segment') {
|
||||
bestCandidate = entry
|
||||
return
|
||||
}
|
||||
if (entry.artifact.type === 'path') {
|
||||
const artifact = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.solid2dId || ''
|
||||
)
|
||||
if (artifact?.type !== 'solid2d') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (!entry.artifact.solid2dId) {
|
||||
console.error(
|
||||
'Expected PathArtifact to have solid2dId, but none found'
|
||||
)
|
||||
return
|
||||
}
|
||||
bestCandidate = {
|
||||
artifact: artifact,
|
||||
selection,
|
||||
id: entry.artifact.solid2dId,
|
||||
}
|
||||
}
|
||||
if (entry.artifact.type === 'plane') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (entry.artifact.type === 'cap') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (entry.artifact.type === 'wall') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
if (type === 'extrude-wall' && entry.artifact.type === 'segment') {
|
||||
if (!entry.artifact.surfaceId) return
|
||||
const wall = engineCommandManager.artifactGraph.get(
|
||||
entry.artifact.surfaceId
|
||||
)
|
||||
if (wall?.type !== 'wall') return
|
||||
bestCandidate = {
|
||||
artifact: wall,
|
||||
selection,
|
||||
id: entry.artifact.surfaceId,
|
||||
}
|
||||
return
|
||||
}
|
||||
if (type === 'edge' && entry.artifact.type === 'segment') {
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: entry.artifact.edgeIds, types: ['sweepEdge'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const edge = [...edges].find(([_, edge]) => edge.type === 'sweepEdge')
|
||||
if (!edge) return
|
||||
bestCandidate = {
|
||||
artifact: edge[1],
|
||||
selection,
|
||||
id: edge[0],
|
||||
}
|
||||
}
|
||||
if (type === 'adjacent-edge' && entry.artifact.type === 'segment') {
|
||||
const edges = getArtifactsOfTypes(
|
||||
{ keys: entry.artifact.edgeIds, types: ['sweepEdge'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const edge = [...edges].find(
|
||||
([_, edge]) =>
|
||||
edge.type === 'sweepEdge' && edge.subType === 'adjacent'
|
||||
)
|
||||
if (!edge) return
|
||||
bestCandidate = {
|
||||
artifact: edge[1],
|
||||
selection,
|
||||
id: edge[0],
|
||||
}
|
||||
}
|
||||
if (
|
||||
(type === 'end-cap' || type === 'start-cap') &&
|
||||
entry.artifact.type === 'path'
|
||||
) {
|
||||
if (!entry.artifact.sweepId) return
|
||||
const extrusion = getArtifactOfTypes(
|
||||
{
|
||||
key: entry.artifact.sweepId,
|
||||
types: ['sweep'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(extrusion)) return
|
||||
const caps = getArtifactsOfTypes(
|
||||
{ keys: extrusion.surfaceIds, types: ['cap'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const cap = [...caps].find(
|
||||
([_, cap]) => cap.subType === (type === 'end-cap' ? 'end' : 'start')
|
||||
)
|
||||
if (!cap) return
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: cap[0],
|
||||
}
|
||||
return
|
||||
}
|
||||
if (entry.artifact.type === 'edgeCut') {
|
||||
const consumedEdge = getArtifactOfTypes(
|
||||
{
|
||||
key: entry.artifact.consumedEdgeId,
|
||||
types: ['segment', 'sweepEdge'],
|
||||
},
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(consumedEdge)) return
|
||||
if (
|
||||
consumedEdge.type === 'segment' &&
|
||||
type === 'base-edgeCut' &&
|
||||
isOverlap(
|
||||
consumedEdge.codeRef.range,
|
||||
selection.secondaryRange || [0, 0]
|
||||
)
|
||||
) {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
} else if (
|
||||
consumedEdge.type === 'sweepEdge' &&
|
||||
((type === 'adjacent-edgeCut' &&
|
||||
consumedEdge.subType === 'adjacent') ||
|
||||
(type === 'opposite-edgeCut' &&
|
||||
consumedEdge.subType === 'opposite'))
|
||||
) {
|
||||
const seg = getArtifactOfTypes(
|
||||
{ key: consumedEdge.segId, types: ['segment'] },
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(seg)) return
|
||||
if (
|
||||
isOverlap(seg.codeRef.range, selection.secondaryRange || [0, 0])
|
||||
) {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (left <= right) {
|
||||
const mid = left + Math.floor((right - left) / 2)
|
||||
const midRange = index[mid].range
|
||||
|
||||
if (entry.artifact.type === 'sweep') {
|
||||
bestCandidate = {
|
||||
artifact: entry.artifact,
|
||||
selection,
|
||||
id: entry.id,
|
||||
}
|
||||
}
|
||||
})
|
||||
if (midRange[0] < targetStart) {
|
||||
// This range starts before our selection, look in right half for later ones
|
||||
lastValidIndex = mid
|
||||
left = mid + 1
|
||||
} else {
|
||||
// This range starts at or after our selection, look in left half
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
|
||||
if (bestCandidate) {
|
||||
return [
|
||||
{
|
||||
type,
|
||||
id: bestCandidate.id,
|
||||
range: bestCandidate.selection.range,
|
||||
},
|
||||
]
|
||||
return lastValidIndex
|
||||
}
|
||||
|
||||
function findOverlappingArtifactsFromIndex(
|
||||
selection: Selection,
|
||||
index: ArtifactIndex
|
||||
): ArtifactEntry[] {
|
||||
if (!selection.codeRef?.range) {
|
||||
console.warn('Selection missing code reference range')
|
||||
return []
|
||||
}
|
||||
|
||||
const selectionRange = selection.codeRef.range
|
||||
const results: ArtifactEntry[] = []
|
||||
|
||||
// Binary search to find the last range where range[0] < selectionRange[0]
|
||||
// This search does not take into consideration the end range, so it's possible
|
||||
// the index it finds dose not have any overlap (depending on the end range)
|
||||
// but it's main purpose is to act as a starting point for the linear part of the search
|
||||
// so a tiny loss in efficiency is acceptable to keep the code simple
|
||||
const startIndex = findLastRangeStartingBefore(index, selectionRange[0])
|
||||
|
||||
// Check all potential overlaps from the found position
|
||||
for (let i = startIndex; i < index.length; i++) {
|
||||
const { range, entry } = index[i]
|
||||
// Stop if we've gone past possible overlaps
|
||||
if (range[0] > selectionRange[1]) break
|
||||
|
||||
if (isOverlap(range, selectionRange)) {
|
||||
results.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function getBestCandidate(
|
||||
entries: ArtifactEntry[],
|
||||
artifactGraph: ArtifactGraph
|
||||
): ArtifactEntry | undefined {
|
||||
if (!entries.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
// Segments take precedence
|
||||
if (entry.artifact.type === 'segment') {
|
||||
return entry
|
||||
}
|
||||
|
||||
// Handle paths and their solid2d references
|
||||
if (entry.artifact.type === 'path') {
|
||||
const solid2dId = entry.artifact.solid2dId
|
||||
if (!solid2dId) {
|
||||
return entry
|
||||
}
|
||||
return [selection]
|
||||
const solid2d = artifactGraph.get(solid2dId)
|
||||
if (solid2d?.type === 'solid2d') {
|
||||
return { id: solid2dId, artifact: solid2d }
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Other valid artifact types
|
||||
if (['plane', 'cap', 'wall', 'sweep'].includes(entry.artifact.type)) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function createSelectionToEngine(
|
||||
selection: Selection,
|
||||
candidateId?: ArtifactId
|
||||
): SelectionToEngine {
|
||||
return {
|
||||
...(candidateId && { id: candidateId }),
|
||||
range: selection.codeRef.range,
|
||||
}
|
||||
}
|
||||
|
||||
export function codeToIdSelections(
|
||||
selections: Selection[],
|
||||
artifactGraph: ArtifactGraph,
|
||||
artifactIndex: ArtifactIndex
|
||||
): SelectionToEngine[] {
|
||||
if (!selections?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!artifactGraph) {
|
||||
console.warn('Artifact graph is missing or empty')
|
||||
return selections.map((selection) => createSelectionToEngine(selection))
|
||||
}
|
||||
|
||||
return selections
|
||||
.flatMap((selection): SelectionToEngine[] => {
|
||||
if (!selection) {
|
||||
console.warn('Null or undefined selection encountered')
|
||||
return []
|
||||
}
|
||||
|
||||
// Direct artifact case
|
||||
if (selection.artifact?.id) {
|
||||
return [createSelectionToEngine(selection, selection.artifact.id)]
|
||||
}
|
||||
|
||||
// Find matching artifacts by code range overlap
|
||||
const overlappingEntries = findOverlappingArtifactsFromIndex(
|
||||
selection,
|
||||
artifactIndex
|
||||
)
|
||||
const bestCandidate = getBestCandidate(overlappingEntries, artifactGraph)
|
||||
|
||||
return [createSelectionToEngine(selection, bestCandidate?.id)]
|
||||
})
|
||||
.filter(isNonNullable)
|
||||
}
|
||||
|
Reference in New Issue
Block a user