Add runtime type checks to r3f (#323)
* Add better error/warning messages to r3f * Fix notifications playground
This commit is contained in:
parent
dee2361c95
commit
965d7085dc
7 changed files with 82 additions and 26 deletions
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import studio, {notify} from '@theatre/studio'
|
import studio from '@theatre/studio'
|
||||||
import {getProject} from '@theatre/core'
|
import {getProject, notify} from '@theatre/core'
|
||||||
import {Scene} from './Scene'
|
import {Scene} from './Scene'
|
||||||
|
|
||||||
studio.initialize()
|
studio.initialize()
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type {EditableFactoryConfig} from './editableFactoryConfigUtils'
|
||||||
import {makeStoreKey} from './utils'
|
import {makeStoreKey} from './utils'
|
||||||
import type {$FixMe, $IntentionalAny} from '../types'
|
import type {$FixMe, $IntentionalAny} from '../types'
|
||||||
import type {ISheetObject} from '@theatre/core'
|
import type {ISheetObject} from '@theatre/core'
|
||||||
|
import {notify} from '@theatre/core'
|
||||||
|
|
||||||
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||||
config: EditableFactoryConfig,
|
config: EditableFactoryConfig,
|
||||||
|
@ -33,6 +34,12 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||||
: {}) &
|
: {}) &
|
||||||
RefAttributes<JSX.IntrinsicElements[U]>
|
RefAttributes<JSX.IntrinsicElements[U]>
|
||||||
|
|
||||||
|
if (Component !== 'primitive' && !type) {
|
||||||
|
throw new Error(
|
||||||
|
`You must provide the type of the component out of which you're creating an editable. For example: editable(MyComponent, 'mesh').`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return forwardRef(
|
return forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
@ -45,6 +52,20 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||||
}: Props,
|
}: Props,
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
//region Runtime type checks
|
||||||
|
if (typeof theatreKey !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
`No valid theatreKey was provided to the editable component. theatreKey must be a string. Received: ${theatreKey}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Component === 'primitive' && !editableType) {
|
||||||
|
throw new Error(
|
||||||
|
`When using the primitive component, you must provide the editableType prop. Received: ${editableType}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
const actualType = type ?? editableType
|
const actualType = type ?? editableType
|
||||||
|
|
||||||
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
const objectRef = useRef<JSX.IntrinsicElements[U]>()
|
||||||
|
@ -75,25 +96,24 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
|
||||||
const dreiComponent =
|
const dreiComponent =
|
||||||
Component.charAt(0).toUpperCase() + Component.slice(1)
|
Component.charAt(0).toUpperCase() + Component.slice(1)
|
||||||
|
|
||||||
console.warn(
|
notify.warning(
|
||||||
`You seem to have declared the camera %c${theatreKey}%c simply as <e.${Component} ... />. This alone won't make r3f use it for rendering.
|
`Possibly incorrect use of <e.${Component} />`,
|
||||||
|
`You seem to have declared the camera "${theatreKey}" simply as \`<e.${Component} ... />\`. This alone won't make r3f use it for rendering.
|
||||||
The easiest way to create a custom animatable ${dreiComponent} is to import it from @react-three/drei, and make it editable.
|
|
||||||
|
|
||||||
%cimport {${dreiComponent}} from '@react-three/drei'
|
|
||||||
const EditableCamera = editable(${dreiComponent}, '${Component}')%c
|
|
||||||
|
|
||||||
|
The easiest way to create a custom animatable \`${dreiComponent}\` is to import it from \`@react-three/drei\`, and make it editable.
|
||||||
|
\`\`\`
|
||||||
|
import {${dreiComponent}} from '@react-three/drei'
|
||||||
|
const EditableCamera =
|
||||||
|
editable(${dreiComponent}, '${Component}')
|
||||||
|
\`\`\`
|
||||||
Then you can use it in your JSX like any other editable component. Note the makeDefault prop exposed by drei, which makes r3f use it for rendering.
|
Then you can use it in your JSX like any other editable component. Note the makeDefault prop exposed by drei, which makes r3f use it for rendering.
|
||||||
|
\`\`\`
|
||||||
%c<EditableCamera
|
<EditableCamera
|
||||||
theatreKey="${theatreKey}"
|
theatreKey="${theatreKey}"
|
||||||
makeDefault
|
makeDefault
|
||||||
>`,
|
>
|
||||||
'font-style: italic;',
|
\`\`\`
|
||||||
'font-style: inherit;',
|
`,
|
||||||
'background: black; color: white;',
|
|
||||||
'background: inherit; color: inherit',
|
|
||||||
'background: black; color: white;',
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [Component, theatreKey])
|
}, [Component, theatreKey])
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type {UnknownShorthandCompoundProps} from '@theatre/core'
|
import type {UnknownShorthandCompoundProps} from '@theatre/core'
|
||||||
|
import {notify} from '@theatre/core'
|
||||||
import {types} from '@theatre/core'
|
import {types} from '@theatre/core'
|
||||||
import type {Object3D} from 'three'
|
import type {Object3D} from 'three'
|
||||||
import type {IconID} from '../extension/icons'
|
import type {IconID} from '../extension/icons'
|
||||||
|
@ -77,11 +78,17 @@ export const createVectorPropConfig = (
|
||||||
z: propValue.z,
|
z: propValue.z,
|
||||||
}
|
}
|
||||||
: // show a warning and return defaultValue
|
: // show a warning and return defaultValue
|
||||||
(console.warn(
|
(notify.warning(
|
||||||
`Couldn't parse prop %c${key}={${JSON.stringify(
|
`Invalid value for vector prop "${key}"`,
|
||||||
|
`Couldn't make sense of \`${key}={${JSON.stringify(
|
||||||
propValue,
|
propValue,
|
||||||
)}}%c, falling back to default value.`,
|
)}}\`, falling back to \`${key}={${JSON.stringify([
|
||||||
'background: black; color: white',
|
defaultValue.x,
|
||||||
|
defaultValue.y,
|
||||||
|
defaultValue.z,
|
||||||
|
])}}\`.
|
||||||
|
|
||||||
|
To fix this, make sure the prop is set to either a number, an array of numbers, or a three.js Vector3 object.`,
|
||||||
),
|
),
|
||||||
defaultValue)
|
defaultValue)
|
||||||
;(['x', 'y', 'z'] as const).forEach((axis) => {
|
;(['x', 'y', 'z'] as const).forEach((axis) => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
|
||||||
import coreTicker from './coreTicker'
|
import coreTicker from './coreTicker'
|
||||||
import type {ProjectId} from '@theatre/shared/utils/ids'
|
import type {ProjectId} from '@theatre/shared/utils/ids'
|
||||||
import {_coreLogger} from './_coreLogger'
|
import {_coreLogger} from './_coreLogger'
|
||||||
|
export {notify} from '@theatre/shared/notify'
|
||||||
export {types}
|
export {types}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logger from './logger'
|
||||||
import * as globalVariableNames from './globalVariableNames'
|
import * as globalVariableNames from './globalVariableNames'
|
||||||
|
|
||||||
export type Notification = {title: string; message: string}
|
export type Notification = {title: string; message: string}
|
||||||
export type NotificationType = 'info' | 'success' | 'warning'
|
export type NotificationType = 'info' | 'success' | 'warning' | 'error'
|
||||||
export type Notify = (
|
export type Notify = (
|
||||||
/**
|
/**
|
||||||
* The title of the notification.
|
* The title of the notification.
|
||||||
|
@ -37,13 +37,31 @@ export type Notifiers = {
|
||||||
* Show an info notification.
|
* Show an info notification.
|
||||||
*/
|
*/
|
||||||
info: Notify
|
info: Notify
|
||||||
|
/**
|
||||||
|
* Show an error notification.
|
||||||
|
*/
|
||||||
|
error: Notify
|
||||||
}
|
}
|
||||||
|
|
||||||
const createHandler =
|
const createHandler =
|
||||||
(type: NotificationType): Notify =>
|
(type: NotificationType): Notify =>
|
||||||
(...args) => {
|
(...args) => {
|
||||||
if (type === 'warning') {
|
switch (type) {
|
||||||
logger.warn(args[1])
|
case 'success': {
|
||||||
|
logger.debug(args.slice(0, 2).join('\n'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'info': {
|
||||||
|
logger.debug(args.slice(0, 2).join('\n'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'warning': {
|
||||||
|
logger.warn(args.slice(0, 2).join('\n'))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
// don't log errors, they're already logged by the browser
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
@ -54,4 +72,12 @@ export const notify: Notifiers = {
|
||||||
warning: createHandler('warning'),
|
warning: createHandler('warning'),
|
||||||
success: createHandler('success'),
|
success: createHandler('success'),
|
||||||
info: createHandler('info'),
|
info: createHandler('info'),
|
||||||
|
error: createHandler('error'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window?.addEventListener('error', (e) => {
|
||||||
|
notify.error(
|
||||||
|
`An error occurred`,
|
||||||
|
`${e.message}\n\nSee **console** for details.`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
|
@ -78,7 +78,6 @@ import {notify} from '@theatre/studio/notify'
|
||||||
window[globalVariableNames.notifications] = {
|
window[globalVariableNames.notifications] = {
|
||||||
notify,
|
notify,
|
||||||
}
|
}
|
||||||
export {notify}
|
|
||||||
|
|
||||||
export type {IScrub} from '@theatre/studio/Scrub'
|
export type {IScrub} from '@theatre/studio/Scrub'
|
||||||
export type {
|
export type {
|
||||||
|
|
|
@ -135,6 +135,7 @@ const NotificationMessage = styled.div`
|
||||||
}
|
}
|
||||||
|
|
||||||
.code {
|
.code {
|
||||||
|
overflow: auto;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: rgba(0, 0, 0, 0.3);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
@ -175,6 +176,7 @@ const COLORS = {
|
||||||
info: '#3b82f6',
|
info: '#3b82f6',
|
||||||
success: '#10b981',
|
success: '#10b981',
|
||||||
warning: '#f59e0b',
|
warning: '#f59e0b',
|
||||||
|
error: '#ef4444',
|
||||||
}
|
}
|
||||||
|
|
||||||
const IndicatorDot = styled.div<{type: NotificationType}>`
|
const IndicatorDot = styled.div<{type: NotificationType}>`
|
||||||
|
@ -214,7 +216,7 @@ const massageMessage = (message: string) => {
|
||||||
* Creates handlers for different types of notifications.
|
* Creates handlers for different types of notifications.
|
||||||
*/
|
*/
|
||||||
const createHandler =
|
const createHandler =
|
||||||
(type: 'warning' | 'success' | 'info'): Notify =>
|
(type: NotificationType): Notify =>
|
||||||
(title, message, docs = [], allowDuplicates = false) => {
|
(title, message, docs = [], allowDuplicates = false) => {
|
||||||
// We can disallow duplicates. We do this through checking the notification contents
|
// We can disallow duplicates. We do this through checking the notification contents
|
||||||
// against a registry of already displayed notifications.
|
// against a registry of already displayed notifications.
|
||||||
|
@ -273,6 +275,7 @@ export const notify: Notifiers = {
|
||||||
warning: createHandler('warning'),
|
warning: createHandler('warning'),
|
||||||
success: createHandler('success'),
|
success: createHandler('success'),
|
||||||
info: createHandler('info'),
|
info: createHandler('info'),
|
||||||
|
error: createHandler('error'),
|
||||||
}
|
}
|
||||||
|
|
||||||
//region Styles
|
//region Styles
|
||||||
|
|
Loading…
Reference in a new issue