Files
modeling-app/src/useStore.ts

454 lines
13 KiB
TypeScript
Raw Normal View History

import { create } from 'zustand'
import { persist } from 'zustand/middleware'
2022-11-26 08:34:23 +11:00
import { addLineHighlight, EditorView } from './editor/highlightextension'
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
import { Program } from './lang/abstractSyntaxTreeTypes'
2023-03-03 20:35:48 +11:00
import { getNodeFromPath } from './lang/queryAst'
import {
ProgramMemory,
Position,
PathToNode,
Rotation,
SourceRange,
} from './lang/executor'
import { recast } from './lang/recast'
import { asyncLexer } from './lang/tokeniser'
import { EditorSelection } from '@codemirror/state'
2023-06-19 10:16:45 +10:00
import { BaseDirectory } from '@tauri-apps/api/fs'
import {
ArtifactMap,
SourceRangeMap,
EngineCommandManager,
} from './lang/std/engineConnection'
import { KCLError, KCLUndefinedValueError } from './lang/errors'
export type Selection = {
type: 'default' | 'line-end' | 'line-mid'
range: SourceRange
}
export type Selections = {
otherSelections: ('y-axis' | 'x-axis' | 'z-axis')[]
codeBasedSelections: Selection[]
}
export type TooTip =
| 'lineTo'
| 'line'
| 'angledLine'
| 'angledLineOfXLength'
| 'angledLineOfYLength'
| 'angledLineToX'
| 'angledLineToY'
| 'xLine'
| 'yLine'
| 'xLineTo'
| 'yLineTo'
| 'angledLineThatIntersects'
export const toolTips: TooTip[] = [
'lineTo',
'line',
'angledLine',
'angledLineOfXLength',
'angledLineOfYLength',
'angledLineToX',
'angledLineToY',
'xLine',
'yLine',
'xLineTo',
'yLineTo',
'angledLineThatIntersects',
]
export type GuiModes =
| {
mode: 'default'
}
| {
mode: 'sketch'
sketchMode: TooTip
isTooltip: true
rotation: Rotation
2023-01-04 01:28:26 +11:00
position: Position
id?: string
2022-12-06 05:40:05 +11:00
pathToNode: PathToNode
}
2022-12-06 05:40:05 +11:00
| {
mode: 'sketch'
sketchMode: 'sketchEdit'
rotation: Rotation
2023-01-04 01:28:26 +11:00
position: Position
2022-12-06 05:40:05 +11:00
pathToNode: PathToNode
2022-12-23 07:37:42 +11:00
}
| {
mode: 'sketch'
sketchMode: 'selectFace'
}
| {
2022-12-06 05:40:05 +11:00
mode: 'canEditSketch'
pathToNode: PathToNode
rotation: Rotation
2023-01-04 01:28:26 +11:00
position: Position
2022-12-23 07:37:42 +11:00
}
| {
mode: 'canEditExtrude'
pathToNode: PathToNode
rotation: Rotation
position: Position
}
type UnitSystem = 'imperial' | 'metric'
export enum Themes {
Light = 'light',
Dark = 'dark',
System = 'system',
}
export const baseUnits: Record<UnitSystem, string[]> = {
imperial: ['in', 'ft'],
metric: ['mm', 'cm', 'm'],
}
2023-06-19 10:16:45 +10:00
interface DefaultDir {
base?: BaseDirectory
dir: string
}
export type PaneType = 'code' | 'variables' | 'debug' | 'kclErrors' | 'logs'
2023-07-27 18:59:40 -04:00
// TODO: import real OpenAPI User type from schema
export interface User {
company?: string
created_at: string
email: string
first_name?: string
id: string
image?: string
last_name?: string
name?: string
phone?: string
updated_at: string
}
export interface StoreState {
2022-11-26 08:34:23 +11:00
editorView: EditorView | null
setEditorView: (editorView: EditorView) => void
highlightRange: [number, number]
setHighlightRange: (range: Selection['range']) => void
setCursor: (selections: Selections) => void
2023-08-09 20:49:10 +10:00
setCursor2: (a?: Selection) => void
selectionRanges: Selections
selectionRangeTypeMap: { [key: number]: Selection['type'] }
setSelectionRanges: (range: Selections) => void
guiMode: GuiModes
lastGuiMode: GuiModes
setGuiMode: (guiMode: GuiModes) => void
logs: string[]
addLog: (log: string) => void
2023-07-26 18:16:20 -05:00
resetLogs: () => void
kclErrors: KCLError[]
addKCLError: (err: KCLError) => void
2023-07-26 18:16:20 -05:00
resetKCLErrors: () => void
ast: Program | null
setAst: (ast: Program | null) => void
updateAst: (
ast: Program,
optionalParams?: {
focusPath?: PathToNode
callBack?: (ast: Program) => void
}
) => void
updateAstAsync: (ast: Program, focusPath?: PathToNode) => void
code: string
setCode: (code: string) => void
2022-11-28 19:43:20 +11:00
formatCode: () => void
2022-12-06 05:40:05 +11:00
errorState: {
isError: boolean
error: string
}
setError: (error?: string) => void
2023-01-04 01:28:26 +11:00
programMemory: ProgramMemory
setProgramMemory: (programMemory: ProgramMemory) => void
isShiftDown: boolean
setIsShiftDown: (isShiftDown: boolean) => void
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
setArtifactNSourceRangeMaps: (a: {
artifactMap: ArtifactMap
sourceRangeMap: SourceRangeMap
}) => void
engineCommandManager?: EngineCommandManager
setEngineCommandManager: (engineCommandManager: EngineCommandManager) => void
mediaStream?: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
isStreamReady: boolean
setIsStreamReady: (isStreamReady: boolean) => void
isMouseDownInStream: boolean
setIsMouseDownInStream: (isMouseDownInStream: boolean) => void
2023-08-09 20:49:10 +10:00
didDragInStream: boolean
setDidDragInStream: (didDragInStream: boolean) => void
cmdId?: string
setCmdId: (cmdId: string) => void
fileId: string
setFileId: (fileId: string) => void
2023-08-09 20:49:10 +10:00
streamDimensions: { streamWidth: number; streamHeight: number }
setStreamDimensions: (dimensions: {
streamWidth: number
streamHeight: number
}) => void
2023-06-19 10:16:45 +10:00
// tauri specific app settings
defaultDir: DefaultDir
setDefaultDir: (dir: DefaultDir) => void
defaultProjectName: string
setDefaultProjectName: (defaultProjectName: string) => void
defaultUnitSystem: UnitSystem
setDefaultUnitSystem: (defaultUnitSystem: UnitSystem) => void
defaultBaseUnit: string
setDefaultBaseUnit: (defaultBaseUnit: string) => void
2023-06-19 10:16:45 +10:00
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
onboardingStatus: string
setOnboardingStatus: (status: string) => void
theme: Themes
setTheme: (theme: Themes) => void
openPanes: PaneType[]
setOpenPanes: (panes: PaneType[]) => void
2023-06-19 10:16:45 +10:00
homeMenuItems: {
name: string
path: string
}[]
setHomeMenuItems: (items: { name: string; path: string }[]) => void
2023-07-11 20:34:09 +10:00
token: string
setToken: (token: string) => void
2023-07-27 18:59:40 -04:00
user?: User
setUser: (user: User | undefined) => void
debugPanel: boolean
setDebugPanel: (debugPanel: boolean) => void
}
let pendingAstUpdates: number[] = []
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
editorView: null,
setEditorView: (editorView) => {
set({ editorView })
},
highlightRange: [0, 0],
setHighlightRange: (selection) => {
set({ highlightRange: selection })
const editorView = get().editorView
if (editorView) {
editorView.dispatch({ effects: addLineHighlight.of(selection) })
}
},
setCursor: (selections) => {
const { editorView } = get()
if (!editorView) return
const ranges: ReturnType<typeof EditorSelection.cursor>[] = []
const selectionRangeTypeMap: { [key: number]: Selection['type'] } = {}
set({ selectionRangeTypeMap })
selections.codeBasedSelections.forEach(({ range, type }) => {
if (range?.[1]) {
ranges.push(EditorSelection.cursor(range[1]))
selectionRangeTypeMap[range[1]] = type
}
})
setTimeout(() => {
editorView.dispatch({
selection: EditorSelection.create(
ranges,
selections.codeBasedSelections.length - 1
),
})
})
},
setCursor2: (codeSelections) => {
const currestSelections = get().selectionRanges
2023-08-09 20:49:10 +10:00
const code = get().code
if (!codeSelections) {
get().setCursor({
otherSelections: currestSelections.otherSelections,
codeBasedSelections: [
{ range: [0, code.length - 1], type: 'default' },
],
})
return
}
const selections: Selections = {
...currestSelections,
codeBasedSelections: get().isShiftDown
? [...currestSelections.codeBasedSelections, codeSelections]
: [codeSelections],
}
get().setCursor(selections)
},
selectionRangeTypeMap: {},
selectionRanges: {
otherSelections: [],
codeBasedSelections: [],
},
setSelectionRanges: (selectionRanges) =>
set({ selectionRanges, selectionRangeTypeMap: {} }),
guiMode: { mode: 'default' },
lastGuiMode: { mode: 'default' },
setGuiMode: (guiMode) => {
set({ guiMode })
},
logs: [],
addLog: (log) => {
if (Array.isArray(log)) {
const cleanLog: any = log.map(({ __geoMeta, ...rest }) => rest)
set((state) => ({ logs: [...state.logs, cleanLog] }))
} else {
set((state) => ({ logs: [...state.logs, log] }))
}
},
2023-07-26 18:16:20 -05:00
resetLogs: () => {
set({ logs: [] })
},
kclErrors: [],
addKCLError: (e) => {
set((state) => ({ kclErrors: [...state.kclErrors, e] }))
},
2023-07-26 18:16:20 -05:00
resetKCLErrors: () => {
set({ kclErrors: [] })
},
ast: null,
setAst: (ast) => {
set({ ast })
},
updateAst: async (ast, { focusPath, callBack = () => {} } = {}) => {
const newCode = recast(ast)
const astWithUpdatedSource = abstractSyntaxTree(
await asyncLexer(newCode)
)
callBack(astWithUpdatedSource)
set({ ast: astWithUpdatedSource, code: newCode })
if (focusPath) {
const { node } = getNodeFromPath<any>(astWithUpdatedSource, focusPath)
const { start, end } = node
if (!start || !end) return
setTimeout(() => {
get().setCursor({
codeBasedSelections: [
{
type: 'default',
range: [start, end],
},
],
otherSelections: [],
})
})
}
},
updateAstAsync: async (ast, focusPath) => {
// clear any pending updates
pendingAstUpdates.forEach((id) => clearTimeout(id))
pendingAstUpdates = []
// setup a new update
pendingAstUpdates.push(
setTimeout(() => {
get().updateAst(ast, { focusPath })
}, 100) as unknown as number
)
},
code: '',
setCode: (code) => {
set({ code })
},
formatCode: async () => {
const code = get().code
const ast = abstractSyntaxTree(await asyncLexer(code))
const newCode = recast(ast)
set({ code: newCode, ast })
},
errorState: {
isError: false,
error: '',
},
setError: (error = '') => {
set({ errorState: { isError: !!error, error } })
},
programMemory: { root: {}, pendingMemory: {} },
setProgramMemory: (programMemory) => set({ programMemory }),
isShiftDown: false,
setIsShiftDown: (isShiftDown) => set({ isShiftDown }),
artifactMap: {},
sourceRangeMap: {},
setArtifactNSourceRangeMaps: (maps) => set({ ...maps }),
setEngineCommandManager: (engineCommandManager) =>
set({ engineCommandManager }),
setMediaStream: (mediaStream) => set({ mediaStream }),
isStreamReady: false,
setIsStreamReady: (isStreamReady) => set({ isStreamReady }),
isMouseDownInStream: false,
setIsMouseDownInStream: (isMouseDownInStream) => {
set({ isMouseDownInStream })
},
2023-08-09 20:49:10 +10:00
didDragInStream: false,
setDidDragInStream: (didDragInStream) => {
set({ didDragInStream })
},
// For stream event handling
cmdId: undefined,
setCmdId: (cmdId) => set({ cmdId }),
fileId: '',
setFileId: (fileId) => set({ fileId }),
2023-08-09 20:49:10 +10:00
streamDimensions: { streamWidth: 1280, streamHeight: 720 },
setStreamDimensions: (streamDimensions) => set({ streamDimensions }),
2023-06-19 10:16:45 +10:00
// tauri specific app settings
defaultDir: {
dir: '',
2023-06-19 10:16:45 +10:00
},
setDefaultDir: (dir) => set({ defaultDir: dir }),
defaultProjectName: 'new-project-$nnn',
setDefaultProjectName: (defaultProjectName) =>
set({ defaultProjectName }),
defaultUnitSystem: 'imperial',
setDefaultUnitSystem: (defaultUnitSystem) => set({ defaultUnitSystem }),
defaultBaseUnit: 'in',
setDefaultBaseUnit: (defaultBaseUnit) => set({ defaultBaseUnit }),
onboardingStatus: '',
setOnboardingStatus: (onboardingStatus) => set({ onboardingStatus }),
theme: Themes.System,
setTheme: (theme) => set({ theme }),
openPanes: ['code'],
setOpenPanes: (openPanes) => set({ openPanes }),
2023-06-19 10:16:45 +10:00
showHomeMenu: true,
setHomeShowMenu: (showHomeMenu) => set({ showHomeMenu }),
homeMenuItems: [],
setHomeMenuItems: (homeMenuItems) => set({ homeMenuItems }),
2023-07-11 20:34:09 +10:00
token: '',
setToken: (token) => set({ token }),
2023-07-27 18:59:40 -04:00
user: undefined,
setUser: (user) => set({ user }),
debugPanel: false,
setDebugPanel: (debugPanel) => set({ debugPanel }),
}),
{
name: 'store',
partialize: (state) =>
Object.fromEntries(
Object.entries(state).filter(([key]) =>
[
'code',
'defaultDir',
'defaultProjectName',
'defaultUnitSystem',
'defaultBaseUnit',
'token',
'debugPanel',
'onboardingStatus',
'theme',
'openPanes',
].includes(key)
)
),
}
)
)