397 lines
9.9 KiB
TypeScript
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
|
|
}
|