Make @theatre/r3f play well with different vs of react,three,r3f #177

This commit is contained in:
Aria 2022-05-27 21:59:51 +02:00 committed by GitHub
parent e8c440f357
commit 9b4aa4b0e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 796 additions and 145 deletions

View file

@ -2,7 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import studio from '@theatre/studio'
import {extension} from '@theatre/r3f'
import extension from '@theatre/r3f/dist/extension'
studio.extend(extension)
studio.initialize()

View file

@ -2,7 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import studio from '@theatre/studio'
import {extension} from '@theatre/r3f'
import extension from '@theatre/r3f/dist/extension'
studio.extend(extension)
studio.initialize()

View file

@ -0,0 +1,74 @@
import path = require('path')
import {build} from 'esbuild'
const definedGlobals = {
'process.env.version': JSON.stringify(require('../package.json').version),
'process.env.NODE_ENV': JSON.stringify('production'),
}
createBundles()
async function createBundles() {
createMainBundle()
createExtensionBundle()
async function createMainBundle() {
const pathToEntry = path.join(__dirname, '../src/index.ts')
const esbuildConfig: Parameters<typeof build>[0] = {
entryPoints: [pathToEntry],
target: ['es6'],
loader: {'.svg': 'text', '.png': 'dataurl'},
bundle: true,
sourcemap: true,
define: {...definedGlobals},
external: [
'@theatre/core',
'@theatre/dataverse',
'@theatre/react',
'@theatre/studio',
'react',
'react-dom',
'three',
'@react-three/fiber',
],
platform: 'browser',
mainFields: ['browser', 'module', 'main'],
conditions: ['browser', 'node'],
outfile: path.join(__dirname, '../dist/index.js'),
format: 'cjs',
metafile: true,
}
const result = await build(esbuildConfig)
}
async function createExtensionBundle() {
const pathToEntry = path.join(__dirname, '../src/extension/index.ts')
const esbuildConfig: Parameters<typeof build>[0] = {
entryPoints: [pathToEntry],
target: 'es6',
loader: {'.svg': 'text', '.png': 'dataurl'},
bundle: true,
sourcemap: true,
define: {...definedGlobals},
external: [
'@theatre/core',
'@theatre/studio',
'@theatre/dataverse',
'@theatre/r3f',
// 'three',
// '@react-three/fiber',
// '@react-three/drei',
// 'three-stdlib',
],
platform: 'browser',
mainFields: ['browser', 'module', 'main'],
conditions: ['browser'],
outfile: path.join(__dirname, '../dist/extension/index.js'),
format: 'cjs',
metafile: true,
}
const result = await build(esbuildConfig)
}
}

View file

@ -0,0 +1,3 @@
{
}

View file

@ -21,31 +21,35 @@
},
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"files": [
"dist/**/*"
],
"exports": {
".": "./dist/index.js",
"./extension": "./dist/extension/index.js"
},
"scripts": {
"prepack": "yarn run build",
"typecheck": "yarn run build",
"build": "tsc --build ./tsconfig.json",
"build": "run-s build:ts build:js",
"build:js": "node -r esbuild-register devEnv/bundle.ts",
"build:ts": "tsc --build ./tsconfig.json",
"prepublish": "yarn run build",
"clean": "rm -rf ./dist && rm -f tsconfig.tsbuildinfo"
},
"devDependencies": {
"@react-three/drei": "^7.3.1",
"@theatre/react": "workspace:*",
"@types/jest": "^26.0.23",
"@types/lodash-es": "^4.17.4",
"@types/node": "^15.6.2",
"@types/react": "^17.0.9",
"@types/styled-components": "^5.1.9",
"npm-run-all": "^4.1.5",
"typescript": "^4.4.2"
},
"dependencies": {
"@react-three/drei": "^7.3.1",
"@theatre/react": "workspace:*",
"esbuild": "^0.12.15",
"esbuild-register": "^2.5.0",
"lodash-es": "^4.17.21",
"npm-run-all": "^4.1.5",
"polished": "^4.1.3",
"react-icons": "^4.2.0",
"react-merge-refs": "^1.1.0",
@ -53,14 +57,15 @@
"react-use-measure": "^2.0.4",
"reakit": "^1.3.8",
"styled-components": "^5.3.0",
"typescript": "^4.4.2",
"zustand": "^3.5.1"
},
"peerDependencies": {
"@react-three/fiber": "^7.0.6",
"@react-three/fiber": ">=7.0.6",
"@theatre/core": "*",
"@theatre/studio": "*",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"three": "^0.131.3"
"react": ">=17.0.2",
"react-dom": ">=17.0.2",
"three": ">=0.131.3"
}
}

View file

@ -1,22 +0,0 @@
import type {ComponentProps, ElementType} from 'react'
import React from 'react'
import {useEditorStore} from '../store'
import {createPortal} from '@react-three/fiber'
export type EditorHelperProps<T extends ElementType> = {
component: T
} & ComponentProps<T>
const EditorHelper = <T extends ElementType>({
component: Component,
...props
}: EditorHelperProps<T>) => {
const helpersRoot = useEditorStore((state) => state.helpersRoot)
if (process.env.NODE_ENV === 'development') {
return <>{createPortal(<Component {...props} />, helpersRoot)}</>
} else {
return null
}
}
export default EditorHelper

View file

@ -0,0 +1,11 @@
module.exports = {
rules: {
'no-restricted-syntax': [
'error',
{
selector: `ImportDeclaration[importKind!='type'][source.value=/\\u002Fmain\\u002F/]`,
message: `The extension should not be able to import the internals of main. If you need to use this API, expose it as __private_api from @theatre/r3f/src/index.ts`,
},
],
},
}

View file

@ -2,17 +2,17 @@ import type {Object3D} from 'three'
import type {VFC} from 'react'
import React, {useEffect, useLayoutEffect, useMemo, useState} from 'react'
import {Sphere, Html} from '@react-three/drei'
import {useEditorStore} from '../store'
import shallow from 'zustand/shallow'
import studio from '@theatre/studio'
import {useSelected} from './useSelected'
import {useVal} from '@theatre/react'
import {getEditorSheetObject} from './editorStuff'
import {getEditorSheetObject} from '../editorStuff'
import type {IconID} from '../icons'
import icons from '../icons'
import type {Helper} from '../editableFactoryConfigUtils'
import type {Helper} from '../../main/editableFactoryConfigUtils'
import {invalidate, useFrame, useThree} from '@react-three/fiber'
import {useDragDetector} from './DragDetector'
import useExtensionStore from '../useExtensionStore'
export interface EditableProxyProps {
storeKey: string
@ -21,7 +21,7 @@ export interface EditableProxyProps {
const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
const editorObject = getEditorSheetObject()
const [setSnapshotProxyObject, editables] = useEditorStore(
const [setSnapshotProxyObject, editables] = useExtensionStore(
(state) => [state.setSnapshotProxyObject, state.editables],
shallow,
)
@ -122,7 +122,7 @@ const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
e.stopPropagation()
const theatreObject =
useEditorStore.getState().editables[storeKey].sheetObject
useExtensionStore.getState().editables[storeKey].sheetObject
if (!theatreObject) {
console.log('no theatre object for', storeKey)
@ -169,7 +169,7 @@ const EditableProxy: VFC<EditableProxyProps> = ({storeKey, object}) => {
if (e.delta < 2) {
e.stopPropagation()
const theatreObject =
useEditorStore.getState().editables[storeKey].sheetObject
useExtensionStore.getState().editables[storeKey].sheetObject
if (!theatreObject) {
console.log('no theatre object for', storeKey)

View file

@ -1,7 +1,6 @@
import type {VFC} from 'react'
import React, {useLayoutEffect, useMemo, useRef, useState} from 'react'
import type {Editable} from '../store'
import {useEditorStore} from '../store'
import type {Editable} from '../../main/store'
import {createPortal} from '@react-three/fiber'
import EditableProxy from './EditableProxy'
import type {OrbitControls} from 'three-stdlib'
@ -13,7 +12,8 @@ import type {IScrub} from '@theatre/studio'
import studio from '@theatre/studio'
import {useSelected} from './useSelected'
import {useVal} from '@theatre/react'
import {getEditorSheetObject} from './editorStuff'
import {getEditorSheetObject} from '../editorStuff'
import useExtensionStore from '../useExtensionStore'
export interface ProxyManagerProps {
orbitControlsRef: React.MutableRefObject<OrbitControls | null>
@ -28,7 +28,7 @@ type IEditableProxy<T> = {
const ProxyManager: VFC<ProxyManagerProps> = ({orbitControlsRef}) => {
const isBeingEdited = useRef(false)
const editorObject = getEditorSheetObject()
const [sceneSnapshot, editables] = useEditorStore(
const [sceneSnapshot, editables] = useExtensionStore(
(state) => [state.sceneSnapshot, state.editables],
shallow,
)

View file

@ -1,20 +1,27 @@
import type {VFC} from 'react'
import {useMemo} from 'react'
import React, {useEffect, useLayoutEffect, useRef} from 'react'
import {useEditorStore} from '../../store'
import shallow from 'zustand/shallow'
import type {WebGLRenderer} from 'three'
import useMeasure from 'react-use-measure'
import styled, {keyframes} from 'styled-components'
import {TiWarningOutline} from 'react-icons/ti'
// This is ugly, but pure TS doesn't let you do bundler-stuff
import noiseImageUrl from './noiseImage'
import noiseImageUrl from './noise-transparent.png'
import useExtensionStore from '../../useExtensionStore'
import {useVal} from '@theatre/react'
import {getEditorSheetObject} from '../../editorStuff'
import studio from '@theatre/studio'
const Container = styled.div`
position: relative;
width: fit-content;
height: fit-content;
border-radius: 4px;
pointer-events: auto;
cursor: pointer;
overflow: hidden;
&.hidden {
border-radius: 8px;
}
`
const Canvas = styled.canvas`
@ -64,14 +71,27 @@ const Warning = styled.div`
opacity: 0.8;
`
const Dot = styled.div`
width: 8px;
height: 8px;
border-radius: 50%;
background-color: red;
pointer-events: auto;
cursor: pointer;
`
interface ReferenceWindowProps {
height: number
maxHeight: number
maxWidth: number
}
const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
const ReferenceWindow: VFC<ReferenceWindowProps> = ({maxHeight, maxWidth}) => {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [gl] = useEditorStore((state) => [state.gl], shallow)
const visible =
useVal(getEditorSheetObject()?.props.viewport.showReferenceWindow) ?? true
const [gl] = useExtensionStore((state) => [state.gl], shallow)
const [ref, bounds] = useMeasure()
const preserveDrawingBuffer =
@ -85,7 +105,18 @@ const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
}
}, [gl, ref])
const [width, height] = useMemo(() => {
if (!gl) return [0, 0]
const aspectRatio = gl.domElement.width / gl.domElement.height
const width = Math.min(aspectRatio * maxHeight, maxWidth)
const height = width / aspectRatio
return [width, height]
}, [gl, maxWidth, maxHeight])
useEffect(() => {
// if (!visible) return
let animationHandle: number
const draw = (gl: WebGLRenderer) => () => {
animationHandle = requestAnimationFrame(draw(gl))
@ -95,8 +126,6 @@ const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
return
}
const width = (gl.domElement.width / gl.domElement.height) * height
const ctx = canvasRef.current!.getContext('2d')!
// https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing
@ -115,20 +144,29 @@ const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
return () => {
cancelAnimationFrame(animationHandle)
}
}, [gl, height, preserveDrawingBuffer])
}, [gl, width, height, preserveDrawingBuffer])
const toggleVisibility = () => {
studio.transaction(({set}) => {
set(getEditorSheetObject()!.props.viewport.showReferenceWindow, !visible)
})
}
// if (!visible) {
// return <Dot onClick={toggleVisibility} />
// }
return (
<Container>
<Container
onClick={toggleVisibility}
className={`${visible ? 'visible' : 'hidden'}`}
style={{
width: (visible ? width : 12) + 'px',
height: (visible ? height : 12) + 'px',
}}
>
{gl?.domElement && preserveDrawingBuffer ? (
<Canvas
ref={canvasRef}
width={
((bounds.width || gl.domElement.width) /
(bounds.height || gl.domElement.height)) *
height
}
height={height}
/>
<Canvas ref={canvasRef} width={width} height={height} />
) : (
<Static>
<Warning>

View file

@ -1,8 +1,8 @@
import {useCallback, useEffect, useLayoutEffect, useMemo, useState} from 'react'
import React from 'react'
import {Canvas, useThree} from '@react-three/fiber'
import type {BaseSheetObjectType} from '../store'
import {allRegisteredObjects, useEditorStore} from '../store'
import type {BaseSheetObjectType} from '../../main/store'
import {__private_allRegisteredObjects as allRegisteredObjects} from '@theatre/r3f'
import shallow from 'zustand/shallow'
import root from 'react-shadow/styled-components'
import ProxyManager from './ProxyManager'
@ -11,11 +11,13 @@ import {useVal} from '@theatre/react'
import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
import type {ISheet} from '@theatre/core'
import useSnapshotEditorCamera from './useSnapshotEditorCamera'
import {getEditorSheet, getEditorSheetObject} from './editorStuff'
import {getEditorSheet, getEditorSheetObject} from '../editorStuff'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {InfiniteGridHelper} from '../InfiniteGridHelper'
import {DragDetectorProvider} from './DragDetector'
import ReferenceWindow from './ReferenceWindow/ReferenceWindow'
import useExtensionStore from '../useExtensionStore'
import useMeasure from 'react-use-measure'
const GlobalStyle = createGlobalStyle`
:host {
@ -62,7 +64,7 @@ const EditorScene: React.FC<{snapshotEditorSheet: ISheet; paneId: string}> = ({
}
}, [gl, viewportLighting, scene, camera])
const helpersRoot = useEditorStore((state) => state.helpersRoot, shallow)
const helpersRoot = useExtensionStore((state) => state.helpersRoot, shallow)
const showGrid = useVal(editorObject?.props.viewport.showGrid) ?? true
const showAxes = useVal(editorObject?.props.viewport.showAxes) ?? true
@ -128,11 +130,9 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
const snapshotEditorSheet = getEditorSheet()
const paneId = props.paneId
const editorObject = getEditorSheetObject()
const [ref, bounds] = useMeasure()
const showReferenceWindow =
useVal(editorObject?.props.viewport.showReferenceWindow) ?? true
const [sceneSnapshot, createSnapshot] = useEditorStore(
const [sceneSnapshot, createSnapshot] = useExtensionStore(
(state) => [state.sceneSnapshot, state.createSnapshot],
shallow,
)
@ -184,16 +184,17 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
<Wrapper>
<Overlay>
<Tools ref={setToolsContainer} />
{showReferenceWindow && (
<ReferenceWindowContainer>
<ReferenceWindow height={120} />
</ReferenceWindowContainer>
)}
<ReferenceWindowContainer>
<ReferenceWindow
maxHeight={Math.min(bounds.height * 0.3, 120)}
maxWidth={Math.min(bounds.width * 0.4, 200)}
/>
</ReferenceWindowContainer>
</Overlay>
{sceneSnapshot ? (
<>
<CanvasWrapper>
<CanvasWrapper ref={ref}>
<Canvas
onCreated={({gl}) => {
gl.setClearColor('white')

View file

@ -1,9 +1,11 @@
import {useLayoutEffect, useRef, useState} from 'react'
import {allRegisteredObjects} from '../store'
import {
__private_allRegisteredObjects as allRegisteredObjects,
__private_makeStoreKey as makeStoreKey,
} from '@theatre/r3f'
import studio from '@theatre/studio'
import type {ISheetObject} from '@theatre/core'
import type {$IntentionalAny} from '../types'
import {makeStoreKey} from '../utils'
import type {$IntentionalAny} from '../../types'
export function useSelected(): undefined | string {
const [state, set] = useState<string | undefined>(undefined)

View file

@ -1,11 +1,11 @@
import SnapshotEditor from './components/SnapshotEditor'
import type {IExtension} from '@theatre/studio'
import {prism, Ticker, val} from '@theatre/dataverse'
import {getEditorSheetObject} from './components/editorStuff'
import {useEditorStore} from './store'
import {getEditorSheetObject} from './editorStuff'
import ReactDOM from 'react-dom'
import React from 'react'
import type {ToolsetConfig} from '@theatre/studio'
import useExtensionStore from './useExtensionStore'
const io5CameraOutline = `<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Camera</title><path d="M350.54 148.68l-26.62-42.06C318.31 100.08 310.62 96 302 96h-92c-8.62 0-16.31 4.08-21.92 10.62l-26.62 42.06C155.85 155.23 148.62 160 140 160H80a32 32 0 00-32 32v192a32 32 0 0032 32h352a32 32 0 0032-32V192a32 32 0 00-32-32h-59c-8.65 0-16.85-4.77-22.46-11.32z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><circle cx="256" cy="272" r="80" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M124 158v-22h-24v22"/></svg>`
const gameIconMove = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 34.47l-90.51 90.51h67.883v108.393H124.98V165.49L34.47 256l90.51 90.51v-67.883h108.393V387.02H165.49L256 477.53l90.51-90.51h-67.883V278.627H387.02v67.883L477.53 256l-90.51-90.51v67.883H278.627V124.98h67.883L256 34.47z"/></svg>`
@ -40,7 +40,7 @@ const r3fExtension: IExtension = {
})
},
'snapshot-editor': (set, studio) => {
const {createSnapshot} = useEditorStore.getState()
const {createSnapshot} = useExtensionStore.getState()
const calc = prism<ToolsetConfig>(() => {
const editorObject = getEditorSheetObject()

View file

@ -0,0 +1,6 @@
import {____private_editorStore} from '@theatre/r3f'
import create from 'zustand'
const useExtensionStore = create(____private_editorStore)
export default useExtensionStore

View file

@ -1,3 +1,7 @@
declare module '*.txt' {
export default string
}
declare module '*.png' {
export default string
}

25
packages/r3f/src/index.ts Normal file
View file

@ -0,0 +1,25 @@
export {default as editable} from './main/editable'
export type {EditableState, BindFunction} from './main/store'
/**
* This is a private API that's exported so that `@theatre/r3f/extension`
* and `@theatre/r3f` can talk to one another. This API _could_ change
* between patch releases, so please don't build on it :)
*
* @internal
*/
export {
editorStore as ____private_editorStore,
allRegisteredObjects as __private_allRegisteredObjects,
} from './main/store'
/**
* This is a private API that's exported so that `@theatre/r3f/extension`
* and `@theatre/r3f` can talk to one another. This API _could_ change
* between patch releases, so please don't build on it :)
*
* @internal
*/
export {makeStoreKey as __private_makeStoreKey} from './main/utils'
export {default as SheetProvider, useCurrentSheet} from './main/SheetProvider'
export {refreshSnapshot} from './main/utils'
export {default as RefreshSnapshot} from './main/RefreshSnapshot'

View file

@ -1,8 +0,0 @@
export {default as extension} from './extension'
export {default as EditorHelper} from './components/EditorHelper'
export type {EditorHelperProps} from './components/EditorHelper'
export {default as editable} from './components/editable'
export type {EditableState, BindFunction} from './store'
export {default as SheetProvider, useCurrentSheet} from './SheetProvider'
export {refreshSnapshot} from './utils'
export {default as RefreshSnapshot} from './components/RefreshSnapshot'

View file

@ -0,0 +1,11 @@
module.exports = {
rules: {
'no-restricted-syntax': [
'error',
{
selector: `ImportDeclaration[importKind!='type'][source.value=/\\u002Fextension\\u002F/]`,
message: `The main bundle should not be able to import the internals of extension.`,
},
],
},
}

View file

@ -1,5 +1,5 @@
import React, {useEffect} from 'react'
import {refreshSnapshot} from '../utils'
import {refreshSnapshot} from './utils'
/**
* Putting this element in a suspense tree makes sure the snapshot editor

View file

@ -1,14 +1,14 @@
import type {ComponentProps, ComponentType, RefAttributes} from 'react'
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
import {allRegisteredObjects, useEditorStore} from '../store'
import {allRegisteredObjects, editorStore} from './store'
import mergeRefs from 'react-merge-refs'
import type {$FixMe} from '@theatre/shared/utils/types'
import type {ISheetObject} from '@theatre/core'
import useInvalidate from './useInvalidate'
import {useCurrentSheet} from '../SheetProvider'
import defaultEditableFactoryConfig from '../defaultEditableFactoryConfig'
import type {EditableFactoryConfig} from '../editableFactoryConfigUtils'
import {makeStoreKey} from '../utils'
import {useCurrentSheet} from './SheetProvider'
import defaultEditableFactoryConfig from './defaultEditableFactoryConfig'
import type {EditableFactoryConfig} from './editableFactoryConfigUtils'
import {makeStoreKey} from './utils'
import type {$FixMe} from '../types'
const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
config: EditableFactoryConfig,
@ -78,7 +78,7 @@ const createEditable = <Keys extends keyof JSX.IntrinsicElements>(
if (objRef) objRef!.current = sheetObject
useEditorStore.getState().addEditable(storeKey, {
editorStore.getState().addEditable(storeKey, {
type: actualType,
sheetObject,
visibleOnlyInEditor: visible === 'editor',

View file

@ -1,7 +1,7 @@
import type {UnknownShorthandCompoundProps} from '@theatre/core'
import {types} from '@theatre/core'
import type {Object3D} from 'three'
import type {IconID} from './icons'
import type {IconID} from '../extension/icons'
import {Color} from 'three'
export type Helper = Object3D & {

View file

@ -1,5 +1,5 @@
import type {StateCreator} from 'zustand'
import create from 'zustand'
import create from 'zustand/vanilla'
import type {Object3D, Scene, WebGLRenderer} from 'three'
import {Group} from 'three'
import type {ISheetObject} from '@theatre/core'
@ -102,7 +102,7 @@ const config: StateCreator<EditorStore> = (set) => {
}
}
export const useEditorStore = create<EditorStore>(config)
export const editorStore = create<EditorStore>(config)
export type BindFunction = (options: {
allowImplicitInstancing?: boolean
@ -111,6 +111,6 @@ export type BindFunction = (options: {
}) => void
export const bindToCanvas: BindFunction = ({gl, scene}) => {
const init = useEditorStore.getState().init
const init = editorStore.getState().init
init(scene, gl)
}

View file

@ -1,7 +1,7 @@
import {useEditorStore} from './store'
import {editorStore} from './store'
import type {ISheet} from '@theatre/core'
export const refreshSnapshot = useEditorStore.getState().createSnapshot
export const refreshSnapshot = editorStore.getState().createSnapshot
export const makeStoreKey = (sheet: ISheet, name: string) =>
`${sheet.address.sheetId}:${sheet.address.sheetInstanceId}:${name}`

View file

@ -5,6 +5,7 @@
"lib": ["ESNext", "DOM"],
"rootDir": "src",
"types": ["jest", "node"],
"emitDeclarationOnly": true,
"composite": true
},
"references": [{"path": "../../theatre"}],