Add runtime type checks to r3f (#323)

* Add better error/warning messages to r3f

* Fix notifications playground
This commit is contained in:
Andrew Prifer 2022-10-21 21:17:45 +02:00 committed by GitHub
parent dee2361c95
commit 965d7085dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 82 additions and 26 deletions

View file

@ -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()

View file

@ -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])

View file

@ -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) => {

View file

@ -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}
/** /**

View file

@ -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.`,
)
})

View file

@ -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 {

View file

@ -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