refactor selections (#876)
* migrate selection types * extract selection event into selections.ts * move code-mirror selection functions into selections.ts * move more selection logit out of code mirror and engine connection * add selection functions pure * tidy up naming * write a novel about how selections work * final comments
This commit is contained in:
@ -36,11 +36,8 @@ import { applyConstraintAngleBetween } from './Toolbar/SetAngleBetween'
|
|||||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { pathMapToSelections } from 'lang/util'
|
import { pathMapToSelections } from 'lang/util'
|
||||||
import {
|
import { useStore } from 'useStore'
|
||||||
dispatchCodeMirrorCursor,
|
import { handleSelectionBatch, handleSelectionWithShift } from 'lib/selections'
|
||||||
setCodeMirrorCursor,
|
|
||||||
useStore,
|
|
||||||
} from 'useStore'
|
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
@ -270,25 +267,37 @@ export const ModelingMachineProvider = ({
|
|||||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
// I've found this the best way to deal with the editor without causing an infinite loop
|
||||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
||||||
// because we want to respect the user manually placing the cursor too.
|
// because we want to respect the user manually placing the cursor too.
|
||||||
const selectionRangeTypeMap = setCodeMirrorCursor({
|
|
||||||
|
// for more details on how selections see `src/lib/selections.ts`.
|
||||||
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||||
|
handleSelectionWithShift({
|
||||||
codeSelection: setSelections.selection,
|
codeSelection: setSelections.selection,
|
||||||
currestSelections: selectionRanges,
|
currestSelections: selectionRanges,
|
||||||
editorView,
|
|
||||||
isShiftDown,
|
isShiftDown,
|
||||||
})
|
})
|
||||||
return {
|
if (codeMirrorSelection) {
|
||||||
selectionRangeTypeMap,
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return { selectionRangeTypeMap }
|
||||||
}
|
}
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
// This DOES NOT set the `selectionRanges` in xstate context
|
||||||
// same as comment above
|
// same as comment above
|
||||||
const { selectionRangeTypeMap } = dispatchCodeMirrorCursor({
|
const { codeMirrorSelection, selectionRangeTypeMap } =
|
||||||
|
handleSelectionBatch({
|
||||||
selections: setSelections.selection,
|
selections: setSelections.selection,
|
||||||
editorView,
|
|
||||||
})
|
})
|
||||||
return {
|
if (codeMirrorSelection) {
|
||||||
selectionRangeTypeMap,
|
setTimeout(() => {
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: codeMirrorSelection,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
return { selectionRangeTypeMap }
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
|
@ -13,7 +13,8 @@ import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
|||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
import { Selections, useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
|
import { processCodeMirrorRanges } from 'lib/selections'
|
||||||
import { LanguageServerClient } from 'editor/lsp'
|
import { LanguageServerClient } from 'editor/lsp'
|
||||||
import kclLanguage from 'editor/lsp/language'
|
import kclLanguage from 'editor/lsp/language'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
@ -132,74 +133,17 @@ export const TextEditor = ({
|
|||||||
if (!editorView) {
|
if (!editorView) {
|
||||||
setEditorView(viewUpdate.view)
|
setEditorView(viewUpdate.view)
|
||||||
}
|
}
|
||||||
const ranges = viewUpdate.state.selection.ranges
|
const eventInfo = processCodeMirrorRanges({
|
||||||
|
codeMirrorRanges: viewUpdate.state.selection.ranges,
|
||||||
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
})
|
||||||
|
if (!eventInfo) return
|
||||||
|
|
||||||
const isChange =
|
send(eventInfo.modelingEvent)
|
||||||
ranges.length !== selectionRanges.codeBasedSelections.length ||
|
eventInfo.engineEvents.forEach((event) =>
|
||||||
ranges.some(({ from, to }, i) => {
|
engineCommandManager.sendSceneCommand(event)
|
||||||
return (
|
|
||||||
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
|
||||||
to !== selectionRanges.codeBasedSelections[i].range[1]
|
|
||||||
)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
if (!isChange) return
|
|
||||||
const codeBasedSelections: Selections['codeBasedSelections'] = ranges.map(
|
|
||||||
({ from, to }) => {
|
|
||||||
if (selectionRangeTypeMap[to]) {
|
|
||||||
return {
|
|
||||||
type: selectionRangeTypeMap[to],
|
|
||||||
range: [from, to],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: 'default',
|
|
||||||
range: [from, to],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const idBasedSelections = codeBasedSelections
|
|
||||||
.map(({ type, range }) => {
|
|
||||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
|
||||||
const entriesWithOverlap = Object.entries(
|
|
||||||
engineCommandManager.artifactMap || {}
|
|
||||||
).filter(([_, artifact]) => {
|
|
||||||
return artifact.range && isOverlap(artifact.range, range)
|
|
||||||
? artifact
|
|
||||||
: false
|
|
||||||
})
|
|
||||||
if (entriesWithOverlap.length) {
|
|
||||||
const [id, artifact] = entriesWithOverlap?.[0]
|
|
||||||
return {
|
|
||||||
type,
|
|
||||||
id:
|
|
||||||
type === 'line-end' &&
|
|
||||||
artifact.type === 'result' &&
|
|
||||||
artifact.headVertexId
|
|
||||||
? artifact.headVertexId
|
|
||||||
: id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.filter(Boolean) as any
|
|
||||||
|
|
||||||
engineCommandManager.cusorsSelected({
|
|
||||||
otherSelections: [],
|
|
||||||
idBasedSelections,
|
|
||||||
})
|
|
||||||
|
|
||||||
selectionRanges &&
|
|
||||||
send({
|
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'mirrorCodeMirrorSelections',
|
|
||||||
selection: {
|
|
||||||
...selectionRanges,
|
|
||||||
codeBasedSelections,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorExtensions = useMemo(() => {
|
const editorExtensions = useMemo(() => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
import { Program, ProgramMemory, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
@ -10,7 +11,6 @@ import {
|
|||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from '../../lang/std/sketchcombos'
|
} from '../../lang/std/sketchcombos'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { Selections } from 'useStore'
|
|
||||||
|
|
||||||
export function horzVertInfo(
|
export function horzVertInfo(
|
||||||
selectionRanges: Selections,
|
selectionRanges: Selections,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, Value } from '../../lang/wasm'
|
import { Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
import { BinaryPart, Program, Value, VariableDeclarator } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -14,7 +14,7 @@ import { GetInfoModal, createInfoModal } from '../SetHorVertDistanceModal'
|
|||||||
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
import { createLiteral, createVariableDeclaration } from '../../lang/modifyAst'
|
||||||
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
import { removeDoubleNegatives } from '../AvailableVarsHelpers'
|
||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { Selections } from 'useStore'
|
import { Selections } from 'lib/selections'
|
||||||
|
|
||||||
const getModalInfo = createInfoModal(GetInfoModal)
|
const getModalInfo = createInfoModal(GetInfoModal)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, toolTips } from '../../useStore'
|
import { toolTips } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
import { BinaryPart, Program, Value } from '../../lang/wasm'
|
||||||
import {
|
import {
|
||||||
getNodePathFromSourceRange,
|
getNodePathFromSourceRange,
|
||||||
|
@ -4,6 +4,7 @@ import { engineCommandManager } from '../lang/std/engineConnection'
|
|||||||
import { useModelingContext } from './useModelingContext'
|
import { useModelingContext } from './useModelingContext'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { SourceRange } from 'lang/wasm'
|
import { SourceRange } from 'lang/wasm'
|
||||||
|
import { getEventForSelectWithPoint } from 'lib/selections'
|
||||||
|
|
||||||
export function useEngineConnectionSubscriptions() {
|
export function useEngineConnectionSubscriptions() {
|
||||||
const { setHighlightRange, highlightRange } = useStore((s) => ({
|
const { setHighlightRange, highlightRange } = useStore((s) => ({
|
||||||
@ -37,63 +38,11 @@ export function useEngineConnectionSubscriptions() {
|
|||||||
})
|
})
|
||||||
const unSubClick = engineCommandManager.subscribeTo({
|
const unSubClick = engineCommandManager.subscribeTo({
|
||||||
event: 'select_with_point',
|
event: 'select_with_point',
|
||||||
callback: ({ data }) => {
|
callback: async (engineEvent) => {
|
||||||
if (!data?.entity_id) {
|
const event = await getEventForSelectWithPoint(engineEvent, {
|
||||||
send({
|
sketchEnginePathId: context.sketchEnginePathId,
|
||||||
type: 'Set selection',
|
|
||||||
data: { selectionType: 'singleCodeCursor' },
|
|
||||||
})
|
})
|
||||||
return
|
send(event)
|
||||||
}
|
|
||||||
const sourceRange =
|
|
||||||
engineCommandManager.artifactMap[data.entity_id]?.range
|
|
||||||
if (engineCommandManager.artifactMap[data.entity_id]) {
|
|
||||||
send({
|
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'singleCodeCursor',
|
|
||||||
selection: { range: sourceRange, type: 'default' },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// selected a vertex
|
|
||||||
engineCommandManager
|
|
||||||
.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
cmd: {
|
|
||||||
type: 'path_get_curve_uuids_for_vertices',
|
|
||||||
vertex_ids: [data.entity_id],
|
|
||||||
path_id: context.sketchEnginePathId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
const curveIds = res?.data?.data?.curve_ids
|
|
||||||
const ranges: RangeAndId[] = curveIds
|
|
||||||
.map(
|
|
||||||
(id: string): RangeAndId => ({
|
|
||||||
id,
|
|
||||||
range: engineCommandManager.artifactMap[id].range,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.sort((a: RangeAndId, b: RangeAndId) => a.range[0] - b.range[0])
|
|
||||||
// default to the head of the curve selected
|
|
||||||
const _sourceRange = ranges?.[0].range
|
|
||||||
const artifact = engineCommandManager.artifactMap[ranges?.[0]?.id]
|
|
||||||
if (artifact.type === 'result') {
|
|
||||||
artifact.headVertexId = data.entity_id
|
|
||||||
}
|
|
||||||
send({
|
|
||||||
type: 'Set selection',
|
|
||||||
data: {
|
|
||||||
selectionType: 'singleCodeCursor',
|
|
||||||
// line-end is used to indicate that headVertexId should be sent to the engine as "selected"
|
|
||||||
// not the whole curve
|
|
||||||
selection: { range: _sourceRange, type: 'line-end' },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selections, executeAst, executeCode } from 'useStore'
|
import { executeAst, executeCode } from 'useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import {
|
import {
|
||||||
EngineCommandManager,
|
EngineCommandManager,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selection, ToolTip } from '../useStore'
|
import { ToolTip } from '../useStore'
|
||||||
|
import { Selection } from 'lib/selections'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Selection, ToolTip } from '../useStore'
|
import { ToolTip } from '../useStore'
|
||||||
|
import { Selection } from 'lib/selections'
|
||||||
import {
|
import {
|
||||||
BinaryExpression,
|
BinaryExpression,
|
||||||
Program,
|
Program,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { SourceRange } from 'lang/wasm'
|
import { SourceRange } from 'lang/wasm'
|
||||||
import { Selections } from 'useStore'
|
|
||||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { exportSave } from 'lib/exportSave'
|
import { exportSave } from 'lib/exportSave'
|
||||||
@ -911,30 +910,6 @@ export class EngineCommandManager {
|
|||||||
this.engineConnection?.send(deletCmd)
|
this.engineConnection?.send(deletCmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
cusorsSelected(selections: {
|
|
||||||
otherSelections: Selections['otherSelections']
|
|
||||||
idBasedSelections: { type: string; id: string }[]
|
|
||||||
}) {
|
|
||||||
if (!this.engineConnection?.isReady()) {
|
|
||||||
console.log('engine connection isnt ready')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'select_clear',
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
this.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'select_add',
|
|
||||||
entities: selections.idBasedSelections.map((s) => s.id),
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
sendSceneCommand(command: EngineCommand): Promise<any> {
|
sendSceneCommand(command: EngineCommand): Promise<any> {
|
||||||
if (this.engineConnection === undefined) {
|
if (this.engineConnection === undefined) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
transformAstSketchLines,
|
transformAstSketchLines,
|
||||||
} from './sketchcombos'
|
} from './sketchcombos'
|
||||||
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
import { getSketchSegmentFromSourceRange } from './sketchConstraints'
|
||||||
import { Selection } from '../../useStore'
|
import { Selection } from 'lib/selections'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
@ -7,7 +7,8 @@ import {
|
|||||||
ConstraintType,
|
ConstraintType,
|
||||||
getConstraintLevelFromSourceRange,
|
getConstraintLevelFromSourceRange,
|
||||||
} from './sketchcombos'
|
} from './sketchcombos'
|
||||||
import { Selections, ToolTip } from '../../useStore'
|
import { ToolTip } from '../../useStore'
|
||||||
|
import { Selections } from 'lib/selections'
|
||||||
import { enginelessExecutor } from '../../lib/testHelpers'
|
import { enginelessExecutor } from '../../lib/testHelpers'
|
||||||
|
|
||||||
beforeAll(() => initPromise)
|
beforeAll(() => initPromise)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { TransformCallback } from './stdTypes'
|
import { TransformCallback } from './stdTypes'
|
||||||
import { Selections, toolTips, ToolTip, Selection } from '../../useStore'
|
import { toolTips, ToolTip } from '../../useStore'
|
||||||
|
import { Selections, Selection } from 'lib/selections'
|
||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
Program,
|
Program,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Selections, StoreState } from '../useStore'
|
import { Selections } from 'lib/selections'
|
||||||
import { Program, PathToNode } from './wasm'
|
import { Program, PathToNode } from './wasm'
|
||||||
import { getNodeFromPath } from './queryAst'
|
import { getNodeFromPath } from './queryAst'
|
||||||
import { ArtifactMap } from './std/engineConnection'
|
import { ArtifactMap } from './std/engineConnection'
|
||||||
|
326
src/lib/selections.ts
Normal file
326
src/lib/selections.ts
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { SourceRange } from 'lang/wasm'
|
||||||
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { EditorSelection } from '@codemirror/state'
|
||||||
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
|
import { SelectionRange } from '@uiw/react-codemirror'
|
||||||
|
import { isOverlap } from 'lib/utils'
|
||||||
|
|
||||||
|
/*
|
||||||
|
How selections work is complex due to the nature that we rely on the engine
|
||||||
|
to tell what has been selected after we send a click command. But than the
|
||||||
|
app needs these selections to be based on cursors, therefore the app must
|
||||||
|
be in control of selections. On top of that because we need to set cursor
|
||||||
|
positions in code-mirror for selections, both from app logic, and still
|
||||||
|
allow the user to add multiple cursors like a normal editor, it's best to
|
||||||
|
let code mirror control cursor positions and assosiate those source ranges
|
||||||
|
with entity ids from code-mirror events later.
|
||||||
|
|
||||||
|
So it's a lot of back and forth. conceptually the back and forth is:
|
||||||
|
|
||||||
|
1) we send a click command to the engine
|
||||||
|
2) the engine sends back ids of entities that were clicked
|
||||||
|
3) we associate that source ranges with those ids
|
||||||
|
4) we set the codemirror selection based on those source ranges (taking
|
||||||
|
into account if the user is holding shift to add to current selections
|
||||||
|
or not). we also create and remember a SelectionRangeTypeMap
|
||||||
|
5) Code mirror fires a an event that cursors have changed, we loop through
|
||||||
|
these ranges and associate them with entity ids again with the ArtifactMap,
|
||||||
|
but also we can pick up selection types using the SelectionRangeTypeMap
|
||||||
|
6) we clear all previous selections in the engine and set the new ones
|
||||||
|
|
||||||
|
The above is less likely to get stale but below is some more details,
|
||||||
|
because this wonders all over the code-base, I've tried to centeralise it
|
||||||
|
by putting relevant utils in this file. All of the functions below are
|
||||||
|
pure with the exception of getEventForSelectWithPoint which makes a call
|
||||||
|
to the engine, but it's a query call (not mutation) so I'm okay with this.
|
||||||
|
Actual side effects that change cursors or tell the engine what's selected
|
||||||
|
are still done throughout the in their relevant parts in the codebase.
|
||||||
|
|
||||||
|
In detail:
|
||||||
|
|
||||||
|
1) Click commands are mostly sent in stream.tsx search for
|
||||||
|
"select_with_point"
|
||||||
|
2) The handler for when the engine sends back entitiy ids calls
|
||||||
|
getEventForSelectWithPoint, it fires an XState event to update our
|
||||||
|
selections is xstate context
|
||||||
|
3 and 4) The XState handler for the above uses handleSelectionBatch and
|
||||||
|
handleSelectionWithShift to update the selections in xstate context as
|
||||||
|
well as returning our SelectionRangeTypeMap and a codeMirror specific
|
||||||
|
event to be dispatched.
|
||||||
|
5) The codeMirror handler for changes to the cursor uses
|
||||||
|
processCodeMirrorRanges to associate the ranges back with their original
|
||||||
|
types and the entity ids (the id can vary depending on the type, as
|
||||||
|
there's only one source range for a given segment, but depending on if
|
||||||
|
the user selected the segment directly or the vertex, the id will be
|
||||||
|
different)
|
||||||
|
6) We take all of the ids and create events for the engine with
|
||||||
|
resetAndSetEngineEntitySelectionCmds
|
||||||
|
|
||||||
|
An important note is that if a user changes the cursor directly themselves
|
||||||
|
then they skip directly to step 5, And these selections get a type of
|
||||||
|
"default".
|
||||||
|
|
||||||
|
There are a few more nuances than this, but best to find them in the code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
|
||||||
|
|
||||||
|
export type Selection = {
|
||||||
|
type:
|
||||||
|
| 'default'
|
||||||
|
| 'line-end'
|
||||||
|
| 'line-mid'
|
||||||
|
| 'face'
|
||||||
|
| 'point'
|
||||||
|
| 'edge'
|
||||||
|
| 'line'
|
||||||
|
| 'arc'
|
||||||
|
| 'all'
|
||||||
|
range: SourceRange
|
||||||
|
}
|
||||||
|
export type Selections = {
|
||||||
|
otherSelections: Axis[]
|
||||||
|
codeBasedSelections: Selection[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectionRangeTypeMap {
|
||||||
|
[key: number]: Selection['type']
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RangeAndId {
|
||||||
|
id: string
|
||||||
|
range: SourceRange
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getEventForSelectWithPoint(
|
||||||
|
{
|
||||||
|
data,
|
||||||
|
}: Extract<
|
||||||
|
Models['OkModelingCmdResponse_type'],
|
||||||
|
{ type: 'select_with_point' }
|
||||||
|
>,
|
||||||
|
{ sketchEnginePathId }: { sketchEnginePathId: string }
|
||||||
|
): Promise<ModelingMachineEvent> {
|
||||||
|
if (!data?.entity_id) {
|
||||||
|
return {
|
||||||
|
type: 'Set selection',
|
||||||
|
data: { selectionType: 'singleCodeCursor' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sourceRange = engineCommandManager.artifactMap[data.entity_id]?.range
|
||||||
|
if (engineCommandManager.artifactMap[data.entity_id]) {
|
||||||
|
return {
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'singleCodeCursor',
|
||||||
|
selection: { range: sourceRange, type: 'default' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// selected a vertex
|
||||||
|
const res = await engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
cmd: {
|
||||||
|
type: 'path_get_curve_uuids_for_vertices',
|
||||||
|
vertex_ids: [data.entity_id],
|
||||||
|
path_id: sketchEnginePathId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const curveIds = res?.data?.data?.curve_ids
|
||||||
|
const ranges: RangeAndId[] = curveIds
|
||||||
|
.map(
|
||||||
|
(id: string): RangeAndId => ({
|
||||||
|
id,
|
||||||
|
range: engineCommandManager.artifactMap[id].range,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.sort((a: RangeAndId, b: RangeAndId) => a.range[0] - b.range[0])
|
||||||
|
// default to the head of the curve selected
|
||||||
|
const _sourceRange = ranges?.[0].range
|
||||||
|
const artifact = engineCommandManager.artifactMap[ranges?.[0]?.id]
|
||||||
|
if (artifact.type === 'result') {
|
||||||
|
artifact.headVertexId = data.entity_id
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'singleCodeCursor',
|
||||||
|
// line-end is used to indicate that headVertexId should be sent to the engine as "selected"
|
||||||
|
// not the whole curve
|
||||||
|
selection: { range: _sourceRange, type: 'line-end' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSelectionBatch({
|
||||||
|
selections,
|
||||||
|
}: {
|
||||||
|
selections: Selections
|
||||||
|
}): {
|
||||||
|
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||||
|
codeMirrorSelection?: EditorSelection
|
||||||
|
} {
|
||||||
|
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
||||||
|
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
|
||||||
|
selections.codeBasedSelections.forEach(({ range, type }) => {
|
||||||
|
if (range?.[1]) {
|
||||||
|
ranges.push(EditorSelection.cursor(range[1]))
|
||||||
|
selectionRangeTypeMap[range[1]] = type
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (ranges.length)
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
codeMirrorSelection: EditorSelection.create(
|
||||||
|
ranges,
|
||||||
|
selections.codeBasedSelections.length - 1
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSelectionWithShift({
|
||||||
|
codeSelection,
|
||||||
|
currestSelections,
|
||||||
|
isShiftDown,
|
||||||
|
}: {
|
||||||
|
codeSelection?: Selection
|
||||||
|
currestSelections: Selections
|
||||||
|
isShiftDown: boolean
|
||||||
|
}): {
|
||||||
|
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||||
|
codeMirrorSelection?: EditorSelection
|
||||||
|
} {
|
||||||
|
const code = kclManager.code
|
||||||
|
if (!codeSelection)
|
||||||
|
return handleSelectionBatch({
|
||||||
|
selections: {
|
||||||
|
otherSelections: currestSelections.otherSelections,
|
||||||
|
codeBasedSelections: [
|
||||||
|
{
|
||||||
|
range: [0, code.length ? code.length - 1 : 0],
|
||||||
|
type: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const selections: Selections = {
|
||||||
|
...currestSelections,
|
||||||
|
codeBasedSelections: isShiftDown
|
||||||
|
? [...currestSelections.codeBasedSelections, codeSelection]
|
||||||
|
: [codeSelection],
|
||||||
|
}
|
||||||
|
return handleSelectionBatch({ selections })
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectionToEngine = { type: Selection['type']; id: string }
|
||||||
|
|
||||||
|
export function processCodeMirrorRanges({
|
||||||
|
codeMirrorRanges,
|
||||||
|
selectionRanges,
|
||||||
|
selectionRangeTypeMap,
|
||||||
|
}: {
|
||||||
|
codeMirrorRanges: readonly SelectionRange[]
|
||||||
|
selectionRanges: Selections
|
||||||
|
selectionRangeTypeMap: SelectionRangeTypeMap
|
||||||
|
}): null | {
|
||||||
|
modelingEvent: ModelingMachineEvent
|
||||||
|
engineEvents: Models['WebSocketRequest_type'][]
|
||||||
|
} {
|
||||||
|
const isChange =
|
||||||
|
codeMirrorRanges.length !== selectionRanges.codeBasedSelections.length ||
|
||||||
|
codeMirrorRanges.some(({ from, to }, i) => {
|
||||||
|
return (
|
||||||
|
from !== selectionRanges.codeBasedSelections[i].range[0] ||
|
||||||
|
to !== selectionRanges.codeBasedSelections[i].range[1]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isChange) return null
|
||||||
|
const codeBasedSelections: Selections['codeBasedSelections'] =
|
||||||
|
codeMirrorRanges.map(({ from, to }) => {
|
||||||
|
if (selectionRangeTypeMap[to]) {
|
||||||
|
return {
|
||||||
|
type: selectionRangeTypeMap[to],
|
||||||
|
range: [from, to],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: 'default',
|
||||||
|
range: [from, to],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const idBasedSelections: SelectionToEngine[] = codeBasedSelections
|
||||||
|
.map(({ type, range }): null | SelectionToEngine => {
|
||||||
|
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||||
|
const entriesWithOverlap = Object.entries(
|
||||||
|
engineCommandManager.artifactMap || {}
|
||||||
|
).filter(([_, artifact]) => {
|
||||||
|
return artifact.range && isOverlap(artifact.range, range)
|
||||||
|
? artifact
|
||||||
|
: false
|
||||||
|
})
|
||||||
|
if (entriesWithOverlap.length) {
|
||||||
|
const [id, artifact] = entriesWithOverlap?.[0]
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
id:
|
||||||
|
type === 'line-end' &&
|
||||||
|
artifact.type === 'result' &&
|
||||||
|
artifact.headVertexId
|
||||||
|
? artifact.headVertexId
|
||||||
|
: id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean) as any
|
||||||
|
|
||||||
|
if (!selectionRanges) return null
|
||||||
|
return {
|
||||||
|
modelingEvent: {
|
||||||
|
type: 'Set selection',
|
||||||
|
data: {
|
||||||
|
selectionType: 'mirrorCodeMirrorSelections',
|
||||||
|
selection: {
|
||||||
|
...selectionRanges,
|
||||||
|
codeBasedSelections,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
engineEvents: resetAndSetEngineEntitySelectionCmds(idBasedSelections),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetAndSetEngineEntitySelectionCmds(
|
||||||
|
selections: SelectionToEngine[]
|
||||||
|
): Models['WebSocketRequest_type'][] {
|
||||||
|
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||||
|
console.log('engine connection isnt ready')
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'select_clear',
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'select_add',
|
||||||
|
entities: selections.map(({ id }) => id),
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
@ -1,7 +1,12 @@
|
|||||||
import { PathToNode } from 'lang/wasm'
|
import { PathToNode } from 'lang/wasm'
|
||||||
import { engineCommandManager } from 'lang/std/engineConnection'
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||||
import { isReducedMotion } from 'lang/util'
|
import { isReducedMotion } from 'lang/util'
|
||||||
import { Axis, Selection, SelectionRangeTypeMap, Selections } from 'useStore'
|
import {
|
||||||
|
Axis,
|
||||||
|
Selection,
|
||||||
|
SelectionRangeTypeMap,
|
||||||
|
Selections,
|
||||||
|
} from 'lib/selections'
|
||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||||
@ -59,30 +64,7 @@ export type SetSelections =
|
|||||||
selection: Selections
|
selection: Selections
|
||||||
}
|
}
|
||||||
|
|
||||||
export const modelingMachine = createMachine(
|
export type ModelingMachineEvent =
|
||||||
{
|
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz5x2CJjAEAymDsAASwWGDknNy0DEggLGyRPLECCMrCCuL20qrCRpYagvnaegjZBtLiBqLploIGCtZVyk4u6Fh4UJ7evgAicGERQSx47NG88RxcSaApdcKSNKIKi8ppwsrLwsX60oriqgaWBgbKImpWqi0gru0eXj4EfaE+g5AwY7ETibyzx5lLu1sygM61ERV0+ho0mU4gURlOwihlis2SuN3cnXuvX6L2CJD42FgH2YrEm3B+QnM4mqdhoyPUILqBm2CAaFTS5houQUCMczmubQxXQeAVxQ1QI2JcVJ32SiGUXPEhjBtWklgaokMzIhqVUNHEG0WgjVqkEUN2aMFHWFvlF4WCbzAUq+UwpqRoGQ2pwacPW4JKJxhgYU0kWu3Wymklrc1qx-gGeIJRPo4xlrrlqUEqkqdMKOSRNEOLPWGTVhkswlU0O5fNaMbu3XjYoAZiRKM60+SMwqMgY7Go6mbalCWaIjLCjtJ0n36tUFNHbpjGwBRXDsMAAJxCAGtAuQABYdhLpmY7SPiSzAyOXwGFUQs01BtXVEEV9VSZr89Gxldrzc7vdD2kGISWPLtTwQepBGpEN8lDGhVBDLYdTUfVZzvYFQ3MS4vytBsHieBMglbdsU0+Ttpn4Sk0KzBR1EKdING1EozQqYxajyNQ6MOBchTjO1BhITBMCPMlKJSSNoIsEEllQrlhGQko9RhZEDFUL1vWEUMcLrRcbUeHF7SCISRLI0CxLdRR2ROJQwQQ2RmMQENJD1OxjR5aE+1rAV6yXB4wD4dgNwAVwwIIRjANdRNlCDlGRJU6kfapK0vdYWSnBQJEsfJMtODYrM-XS+MbAKgtCsBwr-KLgNTMDxPlawJ0DKtXNkLl0o2NjNULPMPQ2HSfL0vxd3YA9iDIShMGGwDopPKjSg0fUaDURFFoVbJ7x1S9oOSmQcl2CtRF461ptG-dxFOg8AElGwE4JhiiszpTqt1pB9C8NA8plynVFlyn1EMzVfdTrAU46PEu87IZukUiNCKAAFtItGJ6XXA+a3vVD6vV2Y41IQlkzAMakLCnD0K0jK9wc6SGLpG67G0IsUHpRkDnosjM3oUi91SsRYjmUywWRUDJRFEY0PXzdQrGpunALls6YexZ4jPhpHHrZtH6tKHkJHkLJ1AUGpsofQxxBEWpxayCXFFl2noZXABHYLsCYIJ2FQVBTM1ijLK5Njp20wtPMEUc+yVRC4vgpZlqjXDfIVg9E-3JWCGXZ3XaCBHUAANwqj2vdm9HZg9NjpPUZbIykFkKYNTD1nsGycjt+modb1OAmCFWIimIvtbVMwczF4wtXqRyEDHfVsiNwtahyTU46Kk7W+T1PkBIXcQjARHkaCPON04cghL716kKVdYjErEWFEy9LoXZKsQT7I3jWEFv5Ydh5183tXd-3VANzYAAF7cHYMfVGvtOZggyOkewqkjBqQrOlOBlQqjyCrEoacR145DRXp-XwRBuCwCCiQPAQR-6AJAWuISQQICEjARQJ0ECXpQOqJIawHl0hMizOlfI8xsrqTBHZD084cFCntu3RshDcDEI3KQ3Ae9NyHxoXQ4hE0mE+xYRBGwlh9SIlNEoRQvMFDpRfgacohhIyS0jO-M6q8pFEJIWQsgUAfAn05nCFS8g6j2AOPSIWOo9SZSVBWBSthMpiykLYpO+DiCOLkWQnw+B2CHmYRzbRRsMig2hCI0MwhLymyxrYPI5g5zZEKoNcReDJEPGkbI+RQxNxMEinQ8gwVMAkC3KohhpFNHpIxjSTIGweRV0LMtRSiBTSXkqNyMW8kBzRLboBVOdSnEKIocA0BJkdDGRwFAXA7jtFLGJkoBC5gGhqXqBM0o6kYSRg0EsRCdhLyWEWfY2p8SGn72UcJHZQlsD7MORjZY0EFTAiqIWcwYh0oiB2rINQyIRBaThG82JqyEkKLAM7GhSSoApKBSkN6NgDS+iqDkCxaUdTVhgVpYcfZrBuVRTUghnyyFME6SZagaSYrAsRDzVYZZVjqACSULU8wpxmGROsLMhQDBMuWQ4mRayggbjANnPOQRyCsrXMmPpPLCVi2zOkNQ3oRBIhMVShSlgEpG0RO+coYs3kABk8AVQACqe0wGnDObt1X509QSoQigYGPIUmqdInlRxKCVFyHqU58liwqd+CGK8XW4HdZ68QAAFCUa4ggAEEIAYAgAQQtEBxSSm5XNEuwJzYNHSFINSJqtA6kRBUSxUJMp+ltd5ZNNNU2uqCB6r2F1t7q2CGWyApai0Vo1rVfpsxEIVEOJlLyZpCz1EJqaA0Bx1JPyJdJZ1g7h2YFHTvPNk6S2EIRkwHw64gjuA0fO-VQhgQSBnsPNIDQciExvkqXK9zlpWDike9NQ7M0BHPROotU704uzdumgA7u7ANVbi5CArvsN6Y8EJHCOMLN6ISbxaV2PWpNeF+3yzTRmkdV1cAcAIIGhA6k1T-CRZqXIVc-r2EyFcsWRLESFlAzR09dGGNUBquRLR80pnWrpKaOBSgQQtpKJWYmhQuRGz5rYMcwnwMjoAHKoCCDmkYsBp3lpZkxhk1rrDqDBCleTxZ1JKgVPkukWRGJvLQBqk98Yu4Jl7mh7WTIsq1DNAcFQ7nNolA0PMU4fZZ7uXFuRhOtMfMifEBlyq4nguWXc9SLMBxp66OhMgv4hwZwKmNKcS83nc6Zey3gcTknzIvoQCLcVxpLzmHqNYZEd8rCSDyIUaEFMZZiOXvLbLJ6ssNZy+wRjqg9XVqEOkDIFzTTmGq15O+ujKhwiQqXXYFpJspvlvmxDpDggbKoWAzAtD6HqMqs2VABAIDcDAJ4XAOdUC7nEDAdgABaW7WzMBA7wK96zdF+FvkpjfTkMX9DZWtUsdUCFBOmkMG8y713yEAM2dQh73TnuQ7e5uDcADxC3pIOwV7G4EYA8CCDgnd2hIQ9wFDvLGZEIHFQYUU4dJqt1F4eUD61QwRQXFscHHV2OCKIPtgI+xOnuMJe29j76bvu-f+4DoH3ylfs7J9D+QB2UtVinBxUx44QwWBqycaE4tZd44N8rx7ai1dk4IBTqnNO6cAMZ3r13RvOeoBNzCJFfNkRfpF1Sui2ZtIRs1KpW2Z3KN2Nx-LlxPh1fvc+9rv7X29fZ7ABzrnK30PMaUBtxExxbAekONcqsdRYRNGWBseopxe0UeTuITPeJ8A569z7jc1OOn+4Z0z4HJey9h+5xBSO+ochzHVJVkQ489T5EyIiP0Eb9Hd7SyvfvD7Ip4v3LnzXX28A66L8z3FKTZ8m+JuqE0+SLCXg3wceYtIrDHFUhE53eXe-c-YfDcSnUfP3enQPO-U-B-Y3efGTWySoOKG+bKGwKsaFQJCwI1bKYEG+SFLSAaPtXvY-ZpDcZpXAVpdpTpd3HpCqL3S-AvXXZnMgigqgjpDcIHEnRhR-BAlIQ2GEVaI2IwTUNQDfNzc2PqNIYcNtQA+6JpFpJXagrpVXSgXPEfMfWnKAqfIHVgxQtpDgrg1Q0veAivbWAQhYJYXIEeMQh8EQPWKQ-JM0WQtPZOIIXAYzEiW0IiDAVsdpe6DpdNJjcpa1fJZFa8EZIwQmKcC8UMXGI2aecwA-PSEgOXTgfALeXEKYfzTI+0ILMw16YOakXRMcHkY4SMKcdKTjakPsDYdUVdS8URJeDwA8cIbcDoXInubgRjPg-Qe5fYTjDkA4M5KotSElVYc0HxGxVw1o8gdojI7uRIRjVrdmdre+IMY0CuDQdUYEEVPoqQSoXYaQxFD0BCJwfkDwjAeAWIPtZ9VbDrOkSQGQOQRQFQdQFTRAIHfhaBHkfJI2RHbBZovyMAO4yvYEI1DTKoQ4TUeEUOKlQcCOXROkN6EMLvRZUE-uQ4bMIDVaMFKEdQFkZyI2WoQGYk8seVRWboDE16RFfYJkKEbxWVJHBAZEJfBSRkkMJI1LXBKjY9T1aknnA4EwRFBNKQbAj45jM0SoXA+QJEiwJIvTWbHoT7AUiCYwKU3IcWJdTtBkZzaCSEvJZEdUZIqpXksDWbUzC9GDCAVUmTXRfU-IRQAoeQE4ZkysIMNzeokEdYCwRUiDMdXeS9W0xdcOBzCmcFIcZzD0rkL0hSc4P02jejdgYMyZV+dhU0UUordSYsQjO3WoPKQXI4BM09IzEzXNdga4tre4g4WtBLE0TYm+ced05AmMjzV+TKerXzfkqTBdINMWC8clMwN9OoaQGuIbD-TTasHKJoypKbOxGbTNJrJMlMjrFaAczjJiaXUcqlQsO5YEZE2QEIuVVw9LebWbbLRDDgf+YKYIVVIHAKcIG8kEns9rYwdYAchUcoDBaoU0crPcmcQgzKQsGc4g08rskdC8q8oIB8tpdcFct84mIQ79FQY0Zk2QdQc2A6BUPdEMewTszLUsv1eCiXTIN8BoIGaEVQO+P9BLVdA6XAuQ-HShMHWg0nUPFcgxWzXRLSZacoLSWPUVfIbMXnJ5H-TCRi4PFXD3NQsnDi6rakOkdYSsGeEQUxccixPsIRNTbk00jPNI4yQfeg9il86s-onRMQLSMwaWC1JSERGoj0FQEk-JV5E8o-fS4A9XOS2QC8KQOkF8EMbcpSR+EmC5WwUMU4IgnvWmUghQygpQjg1iz3YyqsyvLbDIFQWoqEKQHTOwsXJ+avDvfjHSucg8dwzwtsZ8lK7WI4UWM1TGcNQsKilCRYc2HYukCxeyWWVI67DoxYuaLWN0XKC8CFYkpdTUqo4JbKJYAofKVE2WWY+YqATonlAazmbA4bFAjaHkWoPY0odIBPKcGwWvHsHCJwIAA */
|
|
||||||
id: 'Modeling',
|
|
||||||
|
|
||||||
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
|
||||||
predictableActionArguments: true,
|
|
||||||
preserveActionOrder: true,
|
|
||||||
|
|
||||||
context: {
|
|
||||||
guiMode: 'default',
|
|
||||||
selection: [] as string[],
|
|
||||||
selectionRanges: {
|
|
||||||
otherSelections: [],
|
|
||||||
codeBasedSelections: [],
|
|
||||||
} as Selections,
|
|
||||||
selectionRangeTypeMap: {} as SelectionRangeTypeMap,
|
|
||||||
sketchPathToNode: null as PathToNode | null, // maybe too specific, and we should have a generic pathToNode, but being specific seems less risky when I'm not sure
|
|
||||||
sketchEnginePathId: '' as string,
|
|
||||||
sketchPlaneId: '' as string,
|
|
||||||
},
|
|
||||||
|
|
||||||
schema: {
|
|
||||||
events: {} as
|
|
||||||
| { type: 'Deselect all' }
|
| { type: 'Deselect all' }
|
||||||
| { type: 'Deselect edge'; data: Selection & { type: 'edge' } }
|
| { type: 'Deselect edge'; data: Selection & { type: 'edge' } }
|
||||||
| { type: 'Deselect axis'; data: Axis }
|
| { type: 'Deselect axis'; data: Axis }
|
||||||
@ -138,8 +120,32 @@ export const modelingMachine = createMachine(
|
|||||||
| { type: 'Constrain equal length' }
|
| { type: 'Constrain equal length' }
|
||||||
| { type: 'Constrain parallel' }
|
| { type: 'Constrain parallel' }
|
||||||
| { type: 'Constrain remove constraints' }
|
| { type: 'Constrain remove constraints' }
|
||||||
| { type: 'extrude intent' },
|
| { type: 'extrude intent' }
|
||||||
// ,
|
|
||||||
|
export const modelingMachine = createMachine(
|
||||||
|
{
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogDMAVgDsAOmEiAjLMGCAHIIAsAJlHKANCACeiaQDZBEsctGrVATmmiaNQQF8H2tBhz5x2CJjAEAymDsAASwWGDknNy0DEggLGyRPLECCMrCCuL20qrCRpYagvnaegjZBtLiBqLploIGCtZVyk4u6Fh4UJ7evgAicGERQSx47NG88RxcSaApdcKSNKIKi8ppwsrLwsX60oriqgaWBgbKImpWqi0gru0eXj4EfaE+g5AwY7ETibyzx5lLu1sygM61ERV0+ho0mU4gURlOwihlis2SuN3cnXuvX6L2CJD42FgH2YrEm3B+QnM4mqdhoyPUILqBm2CAaFTS5houQUCMczmubQxXQeAVxQ1QI2JcVJ32SiGUXPEhjBtWklgaokMzIhqVUNHEG0WgjVqkEUN2aMFHWFvlF4WCbzAUq+UwpqRoGQ2pwacPW4JKJxhgYU0kWu3Wymklrc1qx-gGeIJRPo4xlrrlqUEqkqdMKOSRNEOLPWGTVhkswlU0O5fNaMbu3XjYoAZiRKM60+SMwqMgY7Go6mbalCWaIjLCjtJ0n36tUFNHbpjGwBRXDsMAAJxCAGtAuQABYdhLpmY7SPiSzAyOXwGFUQs01BtXVEEV9VSZr89Gxldrzc7vdD2kGISWPLtTwQepBGpEN8lDGhVBDLYdTUfVZzvYFQ3MS4vytBsHieBMglbdsU0+Ttpn4Sk0KzBR1EKdING1EozQqYxajyNQ6MOBchTjO1BhITBMCPMlKJSSNoIsEEllQrlhGQko9RhZEDFUL1vWEUMcLrRcbUeHF7SCISRLI0CxLdRR2ROJQwQQ2RmMQENJD1OxjR5aE+1rAV6yXB4wD4dgNwAVwwIIRjANdRNlCDlGRJU6kfapK0vdYWSnBQJEsfJMtODYrM-XS+MbAKgtCsBwr-KLgNTMDxPlawJ0DKtXNkLl0o2NjNULPMPQ2HSfL0vxd3YA9iDIShMGGwDopPKjSg0fUaDURFFoVbJ7x1S9oOSmQcl2CtRF461ptG-dxFOg8AElGwE4JhiiszpTqt1pB9C8NA8plynVFlyn1EMzVfdTrAU46PEu87IZukUiNCKAAFtItGJ6XXA+a3vVD6vV2Y41IQlkzAMakLCnD0K0jK9wc6SGLpG67G0IsUHpRkDnosjM3oUi91SsRYjmUywWRUDJRFEY0PXzdQrGpunALls6YexZ4jPhpHHrZtH6tKHkJHkLJ1AUGpsofQxxBEWpxayCXFFl2noZXABHYLsCYIJ2FQVBTM1ijLK5Njp20wtPMEUc+yVRC4vgpZlqjXDfIVg9E-3JWCGXZ3XaCBHUAANwqj2vdm9HZg9NjpPUZbIykFkKYNTD1nsGycjt+modb1OAmCFWIimIvtbVMwczF4wtXqRyEDHfVsiNwtahyTU46Kk7W+T1PkBIXcQjARHkaCPON04cghL716kKVdYjErEWFEy9LoXZKsQT7I3jWEFv5Ydh5183tXd-3VANzYAAF7cHYMfVGvtOZggyOkewqkjBqQrOlOBlQqjyCrEoacR145DRXp-XwRBuCwCCiQPAQR-6AJAWuISQQICEjARQJ0ECXpQOqJIawHl0hMizOlfI8xsrqTBHZD084cFCntu3RshDcDEI3KQ3Ae9NyHxoXQ4hE0mE+xYRBGwlh9SIlNEoRQvMFDpRfgacohhIyS0jO-M6q8pFEJIWQsgUAfAn05nCFS8g6j2AOPSIWOo9SZSVBWBSthMpiykLYpO+DiCOLkWQnw+B2CHmYRzbRRsMig2hCI0MwhLymyxrYPI5g5zZEKoNcReDJEPGkbI+RQxNxMEinQ8gwVMAkC3KohhpFNHpIxjSTIGweRV0LMtRSiBTSXkqNyMW8kBzRLboBVOdSnEKIocA0BJkdDGRwFAXA7jtFLGJkoBC5gGhqXqBM0o6kYSRg0EsRCdhLyWEWfY2p8SGn72UcJHZQlsD7MORjZY0EFTAiqIWcwYh0oiB2rINQyIRBaThG82JqyEkKLAM7GhSSoApKBSkN6NgDS+iqDkCxaUdTVhgVpYcfZrBuVRTUghnyyFME6SZagaSYrAsRDzVYZZVjqACSULU8wpxmGROsLMhQDBMuWQ4mRayggbjANnPOQRyCsrXMmPpPLCVi2zOkNQ3oRBIhMVShSlgEpG0RO+coYs3kABk8AVQACqe0wGnDObt1X509QSoQigYGPIUmqdInlRxKCVFyHqU58liwqd+CGK8XW4HdZ68QAAFCUa4ggAEEIAYAgAQQtEBxSSm5XNEuwJzYNHSFINSJqtA6kRBUSxUJMp+ltd5ZNNNU2uqCB6r2F1t7q2CGWyApai0Vo1rVfpsxEIVEOJlLyZpCz1EJqaA0Bx1JPyJdJZ1g7h2YFHTvPNk6S2EIRkwHw64gjuA0fO-VQhgQSBnsPNIDQciExvkqXK9zlpWDike9NQ7M0BHPROotU704uzdumgA7u7ANVbi5CArvsN6Y8EJHCOMLN6ISbxaV2PWpNeF+3yzTRmkdV1cAcAIIGhA6k1T-CRZqXIVc-r2EyFcsWRLESFlAzR09dGGNUBquRLR80pnWrpKaOBSgQQtpKJWYmhQuRGz5rYMcwnwMjoAHKoCCDmkYsBp3lpZkxhk1rrDqDBCleTxZ1JKgVPkukWRGJvLQBqk98Yu4Jl7mh7WTIsq1DNAcFQ7nNolA0PMU4fZZ7uXFuRhOtMfMifEBlyq4nguWXc9SLMBxp66OhMgv4hwZwKmNKcS83nc6Zey3gcTknzIvoQCLcVxpLzmHqNYZEd8rCSDyIUaEFMZZiOXvLbLJ6ssNZy+wRjqg9XVqEOkDIFzTTmGq15O+ujKhwiQqXXYFpJspvlvmxDpDggbKoWAzAtD6HqMqs2VABAIDcDAJ4XAOdUC7nEDAdgABaW7WzMBA7wK96zdF+FvkpjfTkMX9DZWtUsdUCFBOmkMG8y713yEAM2dQh73TnuQ7e5uDcADxC3pIOwV7G4EYA8CCDgnd2hIQ9wFDvLGZEIHFQYUU4dJqt1F4eUD61QwRQXFscHHV2OCKIPtgI+xOnuMJe29j76bvu-f+4DoH3ylfs7J9D+QB2UtVinBxUx44QwWBqycaE4tZd44N8rx7ai1dk4IBTqnNO6cAMZ3r13RvOeoBNzCJFfNkRfpF1Sui2ZtIRs1KpW2Z3KN2Nx-LlxPh1fvc+9rv7X29fZ7ABzrnK30PMaUBtxExxbAekONcqsdRYRNGWBseopxe0UeTuITPeJ8A569z7jc1OOn+4Z0z4HJey9h+5xBSO+ochzHVJVkQ489T5EyIiP0Eb9Hd7SyvfvD7Ip4v3LnzXX28A66L8z3FKTZ8m+JuqE0+SLCXg3wceYtIrDHFUhE53eXe-c-YfDcSnUfP3enQPO-U-B-Y3efGTWySoOKG+bKGwKsaFQJCwI1bKYEG+SFLSAaPtXvY-ZpDcZpXAVpdpTpd3HpCqL3S-AvXXZnMgigqgjpDcIHEnRhR-BAlIQ2GEVaI2IwTUNQDfNzc2PqNIYcNtQA+6JpFpJXagrpVXSgXPEfMfWnKAqfIHVgxQtpDgrg1Q0veAivbWAQhYJYXIEeMQh8EQPWKQ-JM0WQtPZOIIXAYzEiW0IiDAVsdpe6DpdNJjcpa1fJZFa8EZIwQmKcC8UMXGI2aecwA-PSEgOXTgfALeXEKYfzTI+0ILMw16YOakXRMcHkY4SMKcdKTjakPsDYdUVdS8URJeDwA8cIbcDoXInubgRjPg-Qe5fYTjDkA4M5KotSElVYc0HxGxVw1o8gdojI7uRIRjVrdmdre+IMY0CuDQdUYEEVPoqQSoXYaQxFD0BCJwfkDwjAeAWIPtZ9VbDrOkSQGQOQRQFQdQFTRAIHfhaBHkfJI2RHbBZovyMAO4yvYEI1DTKoQ4TUeEUOKlQcCOXROkN6EMLvRZUE-uQ4bMIDVaMFKEdQFkZyI2WoQGYk8seVRWboDE16RFfYJkKEbxWVJHBAZEJfBSRkkMJI1LXBKjY9T1aknnA4EwRFBNKQbAj45jM0SoXA+QJEiwJIvTWbHoT7AUiCYwKU3IcWJdTtBkZzaCSEvJZEdUZIqpXksDWbUzC9GDCAVUmTXRfU-IRQAoeQE4ZkysIMNzeokEdYCwRUiDMdXeS9W0xdcOBzCmcFIcZzD0rkL0hSc4P02jejdgYMyZV+dhU0UUordSYsQjO3WoPKQXI4BM09IzEzXNdga4tre4g4WtBLE0TYm+ced05AmMjzV+TKerXzfkqTBdINMWC8clMwN9OoaQGuIbD-TTasHKJoypKbOxGbTNJrJMlMjrFaAczjJiaXUcqlQsO5YEZE2QEIuVVw9LebWbbLRDDgf+YKYIVVIHAKcIG8kEns9rYwdYAchUcoDBaoU0crPcmcQgzKQsGc4g08rskdC8q8oIB8tpdcFct84mIQ79FQY0Zk2QdQc2A6BUPdEMewTszLUsv1eCiXTIN8BoIGaEVQO+P9BLVdA6XAuQ-HShMHWg0nUPFcgxWzXRLSZacoLSWPUVfIbMXnJ5H-TCRi4PFXD3NQsnDi6rakOkdYSsGeEQUxccixPsIRNTbk00jPNI4yQfeg9il86s-onRMQLSMwaWC1JSERGoj0FQEk-JV5E8o-fS4A9XOS2QC8KQOkF8EMbcpSR+EmC5WwUMU4IgnvWmUghQygpQjg1iz3YyqsyvLbDIFQWoqEKQHTOwsXJ+avDvfjHSucg8dwzwtsZ8lK7WI4UWM1TGcNQsKilCRYc2HYukCxeyWWVI67DoxYuaLWN0XKC8CFYkpdTUqo4JbKJYAofKVE2WWY+YqATonlAazmbA4bFAjaHkWoPY0odIBPKcGwWvHsHCJwIAA */
|
||||||
|
id: 'Modeling',
|
||||||
|
|
||||||
|
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
||||||
|
predictableActionArguments: true,
|
||||||
|
preserveActionOrder: true,
|
||||||
|
|
||||||
|
context: {
|
||||||
|
guiMode: 'default',
|
||||||
|
selection: [] as string[],
|
||||||
|
selectionRanges: {
|
||||||
|
otherSelections: [],
|
||||||
|
codeBasedSelections: [],
|
||||||
|
} as Selections,
|
||||||
|
selectionRangeTypeMap: {} as SelectionRangeTypeMap,
|
||||||
|
sketchPathToNode: null as PathToNode | null, // maybe too specific, and we should have a generic pathToNode, but being specific seems less risky when I'm not sure
|
||||||
|
sketchEnginePathId: '' as string,
|
||||||
|
sketchPlaneId: '' as string,
|
||||||
|
},
|
||||||
|
|
||||||
|
schema: {
|
||||||
|
events: {} as ModelingMachineEvent,
|
||||||
},
|
},
|
||||||
|
|
||||||
states: {
|
states: {
|
||||||
|
108
src/useStore.ts
108
src/useStore.ts
@ -1,13 +1,8 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
import { addLineHighlight, EditorView } from './editor/highlightextension'
|
import { addLineHighlight, EditorView } from './editor/highlightextension'
|
||||||
import {
|
import { parse, Program, _executor, ProgramMemory } from './lang/wasm'
|
||||||
parse,
|
import { Selection, Selections, SelectionRangeTypeMap } from 'lib/selections'
|
||||||
Program,
|
|
||||||
_executor,
|
|
||||||
ProgramMemory,
|
|
||||||
SourceRange,
|
|
||||||
} from './lang/wasm'
|
|
||||||
import { enginelessExecutor } from './lib/testHelpers'
|
import { enginelessExecutor } from './lib/testHelpers'
|
||||||
import { EditorSelection } from '@codemirror/state'
|
import { EditorSelection } from '@codemirror/state'
|
||||||
import { EngineCommandManager } from './lang/std/engineConnection'
|
import { EngineCommandManager } from './lang/std/engineConnection'
|
||||||
@ -15,25 +10,6 @@ import { KCLError } from './lang/errors'
|
|||||||
import { kclManager } from 'lang/KclSinglton'
|
import { kclManager } from 'lang/KclSinglton'
|
||||||
import { DefaultPlanes } from './wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from './wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
|
|
||||||
export type Axis = 'y-axis' | 'x-axis' | 'z-axis'
|
|
||||||
|
|
||||||
export type Selection = {
|
|
||||||
type:
|
|
||||||
| 'default'
|
|
||||||
| 'line-end'
|
|
||||||
| 'line-mid'
|
|
||||||
| 'face'
|
|
||||||
| 'point'
|
|
||||||
| 'edge'
|
|
||||||
| 'line'
|
|
||||||
| 'arc'
|
|
||||||
| 'all'
|
|
||||||
range: SourceRange
|
|
||||||
}
|
|
||||||
export type Selections = {
|
|
||||||
otherSelections: Axis[]
|
|
||||||
codeBasedSelections: Selection[]
|
|
||||||
}
|
|
||||||
export type ToolTip =
|
export type ToolTip =
|
||||||
| 'lineTo'
|
| 'lineTo'
|
||||||
| 'line'
|
| 'line'
|
||||||
@ -74,10 +50,6 @@ export type PaneType =
|
|||||||
| 'logs'
|
| 'logs'
|
||||||
| 'lspMessages'
|
| 'lspMessages'
|
||||||
|
|
||||||
export interface SelectionRangeTypeMap {
|
|
||||||
[key: number]: Selection['type']
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
editorView: EditorView | null
|
editorView: EditorView | null
|
||||||
setEditorView: (editorView: EditorView) => void
|
setEditorView: (editorView: EditorView) => void
|
||||||
@ -342,79 +314,3 @@ export async function executeAst({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dispatchCodeMirrorCursor({
|
|
||||||
selections,
|
|
||||||
editorView,
|
|
||||||
}: {
|
|
||||||
selections: Selections
|
|
||||||
editorView: EditorView
|
|
||||||
}): {
|
|
||||||
selectionRangeTypeMap: SelectionRangeTypeMap
|
|
||||||
} {
|
|
||||||
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
|
|
||||||
const selectionRangeTypeMap: SelectionRangeTypeMap = {}
|
|
||||||
selections.codeBasedSelections.forEach(({ range, type }) => {
|
|
||||||
if (range?.[1]) {
|
|
||||||
ranges.push(EditorSelection.cursor(range[1]))
|
|
||||||
selectionRangeTypeMap[range[1]] = type
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
ranges.length &&
|
|
||||||
editorView.dispatch({
|
|
||||||
selection: EditorSelection.create(
|
|
||||||
ranges,
|
|
||||||
selections.codeBasedSelections.length - 1
|
|
||||||
),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
selectionRangeTypeMap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCodeMirrorCursor({
|
|
||||||
codeSelection,
|
|
||||||
currestSelections,
|
|
||||||
editorView,
|
|
||||||
isShiftDown,
|
|
||||||
}: {
|
|
||||||
codeSelection?: Selection
|
|
||||||
currestSelections: Selections
|
|
||||||
editorView: EditorView
|
|
||||||
isShiftDown: boolean
|
|
||||||
}): SelectionRangeTypeMap {
|
|
||||||
// This DOES NOT set the `selectionRanges` in xstate context
|
|
||||||
// instead it updates/dispatches to the editor, which in turn updates the xstate context
|
|
||||||
// I've found this the best way to deal with the editor without causing an infinite loop
|
|
||||||
// and really we want the editor to be in charge of cursor positions and for `selectionRanges` mirror it
|
|
||||||
// because we want to respect the user manually placing the cursor too.
|
|
||||||
const code = kclManager.code
|
|
||||||
if (!codeSelection) {
|
|
||||||
const { selectionRangeTypeMap } = dispatchCodeMirrorCursor({
|
|
||||||
editorView,
|
|
||||||
selections: {
|
|
||||||
otherSelections: currestSelections.otherSelections,
|
|
||||||
codeBasedSelections: [
|
|
||||||
{
|
|
||||||
range: [0, code.length ? code.length - 1 : 0],
|
|
||||||
type: 'default',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return selectionRangeTypeMap
|
|
||||||
}
|
|
||||||
const selections: Selections = {
|
|
||||||
...currestSelections,
|
|
||||||
codeBasedSelections: isShiftDown
|
|
||||||
? [...currestSelections.codeBasedSelections, codeSelection]
|
|
||||||
: [codeSelection],
|
|
||||||
}
|
|
||||||
const { selectionRangeTypeMap } = dispatchCodeMirrorCursor({
|
|
||||||
editorView,
|
|
||||||
selections,
|
|
||||||
})
|
|
||||||
return selectionRangeTypeMap
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user