Files
cadhub/app/web/src/helpers/hooks/useIdeState.ts

397 lines
9.9 KiB
TypeScript

import { useReducer } from 'react'
import { cadPackages, initCodeMap, initGuideMap } from 'src/helpers/cadPackages'
import type { RootState } from '@react-three/fiber'
import type {
RawCustomizerParams,
ArtifactTypes,
} from 'src/helpers/cadPackages/common'
import { CadhubParams } from 'src/components/Customizer/customizerConverter'
import { CadPackageType } from 'src/components/CadPackage/CadPackage'
function withThunk(dispatch, getState) {
return (actionOrThunk) =>
typeof actionOrThunk === 'function'
? actionOrThunk(dispatch, getState)
: dispatch(actionOrThunk)
}
const codeStorageKey = 'Last-editor-code'
export const makeCodeStoreKey = (ideType) => `${codeStorageKey}-${ideType}`
let mutableState: State = null
export interface XYZ {
x: number
y: number
z: number
}
export interface Camera {
dist?: number
position?: XYZ
rotation?: XYZ
isScadUpdate?: boolean
}
export interface MosaicTree {
first: string | MosaicTree
second: string | MosaicTree
direction?: string
splitPercentage?: number
}
interface CodeTab {
type: 'code'
label: string
content?: string
}
interface GuideTab {
type: 'guide'
label: string
content: string
}
interface ComponentTab {
type: 'component'
label: string
Component: React.FC
}
type EditorTab = GuideTab | CodeTab | ComponentTab
export interface State {
ideType: 'INIT' | CadPackageType
viewerContext: 'ide' | 'viewer'
ideGuide?: string
consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[]
code: string
editorTabs: EditorTab[]
currentModel: number
objectData: {
type: 'INIT' | ArtifactTypes
data: any // eslint-disable-line @typescript-eslint/no-explicit-any
quality: 'low' | 'high'
}
customizerParams: CadhubParams[]
currentParameters?: RawCustomizerParams
isCustomizerOpen: boolean
layout: MosaicTree
camera: Camera
viewerSize: { width: number; height: number }
isLoading: boolean
threeInstance: RootState
sideTray: string[] // could probably be an array of a union type
}
const code = ''
const initialLayout: MosaicTree = {
direction: 'row',
first: 'Editor',
second: {
direction: 'column',
first: 'Viewer',
second: 'Console',
splitPercentage: 70,
},
}
export const initialState: State = {
ideType: 'INIT',
viewerContext: 'ide',
consoleMessages: [
{ type: 'message', message: 'Initialising', time: new Date() },
],
code,
editorTabs: [{ type: 'code', label: 'Code' }],
currentModel: 0,
objectData: {
type: 'INIT',
data: null,
quality: 'low',
},
customizerParams: [],
isCustomizerOpen: false,
layout: initialLayout,
camera: {},
viewerSize: { width: 0, height: 0 },
isLoading: false,
threeInstance: null,
sideTray: [],
}
const reducer = (state: State, { type, payload }): State => {
switch (type) {
case 'initIde':
return {
...state,
code:
payload.code ||
// localStorage.getItem(makeCodeStoreKey(payload.cadPackage)) ||
initCodeMap[payload.cadPackage] ||
'',
ideType: payload.cadPackage,
ideGuide: initGuideMap[payload.cadPackage],
viewerContext: payload.viewerContext,
}
case 'updateCode':
return { ...state, code: payload }
case 'resetCustomizer': {
const resetParameters = {}
state.customizerParams.forEach(({ name, initial }) => {
resetParameters[name] = initial
})
return {
...state,
currentParameters: resetParameters,
}
}
case 'healthyRender': {
const currentParameters = {}
const customizerParams: CadhubParams[] = payload.customizerParams || []
customizerParams.forEach((param) => {
currentParameters[param.name] =
typeof state?.currentParameters?.[param.name] === 'undefined'
? param.initial
: state?.currentParameters?.[param.name]
})
return {
...state,
objectData: {
...state.objectData,
type: payload.objectData?.type,
data: payload.objectData?.data,
},
customizerParams,
currentParameters,
consoleMessages: payload.message
? [...state.consoleMessages, payload.message]
: payload.message,
isLoading: false,
camera: payload.camera || state.camera,
}
}
case 'errorRender':
return {
...state,
consoleMessages: payload.message
? [...state.consoleMessages, payload.message]
: payload.message,
isLoading: false,
}
case 'setCurrentCustomizerParams':
if (!Object.keys(payload || {}).length) return state
return {
...state,
currentParameters: payload,
}
case 'setCustomizerOpenState':
return {
...state,
isCustomizerOpen: payload,
}
case 'setLayout':
return {
...state,
layout: payload,
}
case 'updateCamera':
return {
...state,
camera: payload.camera,
}
case 'updateViewerSize':
return {
...state,
viewerSize: payload.viewerSize,
}
case 'setLoading':
return {
...state,
isLoading: true,
}
case 'resetLoading':
return {
...state,
isLoading: false,
}
case 'resetLayout':
return {
...state,
layout: initialLayout,
}
case 'setThreeInstance':
return {
...state,
threeInstance: payload,
}
case 'settingsButtonClicked': {
const isReClick =
state.sideTray.length &&
state.sideTray.length === payload.length &&
state.sideTray.every((original, index) => original === payload[index])
const payloadInOriginal =
payload.length && state.sideTray.indexOf(payload[0])
const isAncestorClick =
state.sideTray.length &&
state.sideTray.length > payload.length &&
payloadInOriginal >= 0 &&
payload.every(
(incoming, i) => incoming === state.sideTray[i + payloadInOriginal]
)
if (isReClick) {
return {
...state,
sideTray: state.sideTray.slice(0, -1),
}
} else if (isAncestorClick) {
return {
...state,
sideTray: state.sideTray.slice(0, payload.length * -1 - 1),
}
}
return {
...state,
sideTray: payload,
}
}
case 'switchEditorModel':
return {
...state,
currentModel: payload,
}
case 'addEditorModel':
return {
...state,
editorTabs: [...state.editorTabs, payload],
}
case 'removeEditorModel':
return {
...state,
editorTabs: [
...state.editorTabs.slice(0, payload),
...state.editorTabs.slice(payload + 1),
],
currentModel: payload === 0 ? 0 : payload - 1,
}
// 'updateEditorModel' and 'reorderEditorModels' added during #519 as part of adding editor tabs, this functionality is expected soon.
// https://github.com/Irev-Dev/cadhub/pull/519
// case 'updateEditorModel': {
// const newModels = [...state.models]
// newModels[state.currentModel].content = payload
// return {
// ...state,
// models: newModels,
// }
// }
// case 'reorderEditorModels': {
// const newModels = [
// ...state.models.slice(0, state.currentModel),
// ...state.models.slice(state.currentModel + 1),
// ].splice(payload, 0, state.models[state.currentModel])
// return {
// ...state,
// models: newModels,
// currentModel: payload,
// }
// }
default:
return state
}
}
export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
const [state, dispatch] = useReducer(reducer, initialState)
mutableState = state
const getState = (): State => mutableState
const thunkDispatch = withThunk(dispatch, getState)
return [state, thunkDispatch]
}
interface RequestRenderArgsStateless {
state: State
camera?: State['camera']
viewAll?: boolean
viewerSize?: State['viewerSize']
quality?: State['objectData']['quality']
specialCadProcess?: string
parameters?: { [key: string]: any }
}
export const requestRenderStateless = ({
state,
camera,
viewAll,
viewerSize,
quality = 'low',
specialCadProcess = null,
parameters,
}: RequestRenderArgsStateless): null | Promise<any> => {
if (
!(
state.ideType !== 'INIT' &&
(!state.isLoading || state.objectData?.type === 'INIT')
)
) {
return null
}
const renderFn = specialCadProcess
? cadPackages[state.ideType][specialCadProcess]
: cadPackages[state.ideType].render
return renderFn({
code: state.code,
settings: {
parameters: state.isCustomizerOpen
? parameters || state.currentParameters
: {},
viewAll,
camera: camera || state.camera,
viewerSize: viewerSize || state.viewerSize,
quality,
},
})
}
interface RequestRenderArgs extends RequestRenderArgsStateless {
dispatch: any
}
export const requestRender = ({ dispatch, ...rest }: RequestRenderArgs) => {
const renderPromise = requestRenderStateless(rest)
if (!renderPromise) {
return
}
return renderPromise
.then(
({
objectData,
message,
status,
customizerParams,
currentParameters,
camera,
}) => {
if (status === 'error') {
dispatch({
type: 'errorRender',
payload: { message },
})
} else {
dispatch({
type: 'healthyRender',
payload: {
objectData,
message,
lastRunCode: code,
customizerParams,
currentParameters,
camera,
},
})
return objectData
}
}
)
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
}