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 => { 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 }