feat(remote): multiple-window support

This commit is contained in:
Colin Duffy 2023-07-05 10:56:17 -07:00 committed by Aria
parent 8b8d2bf431
commit bcfb91fbb7
5 changed files with 423 additions and 0 deletions

View file

@ -0,0 +1,85 @@
import React, {useEffect, useRef} from 'react'
import type {CSSProperties} from 'react'
import {types} from '@theatre/core'
import type {ISheet} from '@theatre/core'
import {remote} from './Remote'
// Box element
export const BoxSize = 100
const Box3DCSS: CSSProperties = {
border: '1px solid #999',
position: 'absolute',
width: `${BoxSize}px`,
height: `${BoxSize}px`,
}
const Box3DTextCSS: CSSProperties = {
margin: '0',
padding: '0',
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
textAlign: 'center',
width: '100%',
}
export const Box3D: React.FC<{
sheet: ISheet
name: string
x: number
y: number
}> = ({sheet, name, x, y}) => {
const elementRef = useRef<HTMLDivElement>(null)
// Animation
useEffect(() => {
const element = elementRef.current!
const id = `Box - ${name}`
const sheetObj = remote.sheetObject(
sheet.address.sheetId,
id,
{
background: types.rgba({r: 16 / 255, g: 16 / 255, b: 16 / 255, a: 1}),
opacity: types.number(1, {range: [0, 1]}),
position: {
x: x,
y: y,
z: 0,
},
rotation: {
x: types.number(0, {range: [-360, 360]}),
y: types.number(0, {range: [-360, 360]}),
z: types.number(0, {range: [-360, 360]}),
},
scale: {
x: 1,
y: 1,
z: 1,
},
},
(values: any) => {
const {background, opacity, position, rotation, scale} = values
element.style.backgroundColor = `rgba(${background.r * 255}, ${
background.g * 255
}, ${background.b * 255}, 1)`
element.style.opacity = opacity
const translate3D = `translate3d(${position.x}px, ${position.y}px, ${position.z}px)`
const rotate3D = `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg) rotateZ(${rotation.z}deg)`
const scale3D = `scaleX(${scale.x}) scaleY(${scale.y}) scaleZ(${scale.z})`
const transform = `${scale3D} ${translate3D} ${rotate3D}`
element.style.transform = transform
},
)
return () => {
if (sheetObj !== undefined) remote.unsubscribe(sheetObj)
}
}, [])
return (
<div ref={elementRef} style={Box3DCSS}>
<span style={Box3DTextCSS}>{name}</span>
</div>
)
}

View file

@ -0,0 +1,152 @@
import type {IProject, ISheet, ISheetObject} from '@theatre/core'
import type {VoidFn} from '@theatre/dataverse/src/types'
export type TheatreUpdateCallback = (data: any) => void
export type BroadcastDataEvent =
| 'setSheet'
| 'setSheetObject'
| 'updateSheetObject'
| 'updateTimeline'
export interface BroadcastData {
event: BroadcastDataEvent
data: any
}
export type BroadcastCallback = (data: BroadcastData) => void
// Default SheetObject.onValuesChange callback
const noop: TheatreUpdateCallback = (values: any) => {}
function isColor(obj: any) {
return (
obj.r !== undefined &&
obj.g !== undefined &&
obj.b !== undefined &&
obj.a !== undefined
)
}
// Which hashtag to add to the URL
const hashtag = 'editor'
class RemoteSingleton {
// Remote
mode = 'listener'
channel: BroadcastChannel
// Theatre
project!: IProject
sheets: Map<string, ISheet> = new Map()
sheetObjects: Map<string, ISheetObject> = new Map()
sheetObjectCBs: Map<string, TheatreUpdateCallback> = new Map()
sheetObjectUnsubscribe: Map<string, VoidFn> = new Map()
constructor() {
this.channel = new BroadcastChannel('theatre')
this.showTheatre = document.location.hash.search(hashtag) > -1
}
// Remote
send(data: BroadcastData) {
if (this.mode === 'theatre') {
this.channel.postMessage(data)
}
}
listen(callback: BroadcastCallback) {
if (this.mode === 'listener') {
this.channel.onmessage = (event: MessageEvent<any>) => {
callback(event.data)
}
}
}
// Theatre
sheet(name: string): ISheet {
let sheet: any = this.sheets.get(name)
if (sheet !== undefined) return sheet
sheet = this.project.sheet(name)
this.sheets.set(name, sheet)
return sheet
}
sheetObject(
sheetName: string,
key: string,
props: any,
onUpdate?: TheatreUpdateCallback,
): ISheetObject | undefined {
const sheet = this.sheets.get(sheetName)
if (sheet === undefined) return undefined
const objName = `${sheetName}_${key}`
let obj = this.sheetObjects.get(objName)
if (obj !== undefined) {
obj = sheet.object(key, {...props, ...obj.value}, {reconfigure: true})
return obj
}
obj = sheet.object(key, props)
this.sheetObjects.set(objName, obj)
this.sheetObjectCBs.set(objName, onUpdate !== undefined ? onUpdate : noop)
const unsubscribe = obj.onValuesChange((values: any) => {
if (this.showTheatre) {
for (let i in values) {
const value = values[i]
if (typeof value === 'object') {
if (isColor(value)) {
values[i] = {
r: value.r,
g: value.g,
b: value.b,
a: value.a,
}
}
}
}
this.send({
event: 'updateSheetObject',
data: {
sheetObject: objName,
values: values,
},
})
} else {
const callback = this.sheetObjectCBs.get(objName)
if (callback !== undefined) callback(values)
}
})
this.sheetObjectUnsubscribe.set(objName, unsubscribe)
return obj
}
unsubscribe(sheet: ISheetObject) {
const id = `${sheet.address.sheetId}_${sheet.address.objectKey}`
const unsubscribe = this.sheetObjectUnsubscribe.get(id)
if (unsubscribe !== undefined) {
unsubscribe()
}
}
// Getters / Setters
get showTheatre(): boolean {
return this.mode === 'theatre'
}
set showTheatre(value: boolean) {
if (value) {
this.mode = 'theatre'
document.title += ' - Editor'
}
}
}
export const remote = new RemoteSingleton()

View file

@ -0,0 +1,119 @@
import type {IProject, ISheet} from '@theatre/core'
import studio from '@theatre/studio'
import {remote} from './Remote'
import type {BroadcastData, BroadcastDataEvent} from './Remote'
/**
* Handles the communication between windows
*/
export default function RemoteController(project: IProject) {
let activeSheet: ISheet | undefined = undefined
remote.project = project
/**
* Editor is hidden, this window receives updates
*/
const receiveRemote = () => {
studio.ui.hide()
remote.listen((msg: BroadcastData) => {
switch (msg.event) {
case 'setSheet':
const sheet = remote.sheets.get(msg.data.sheet)
if (sheet !== undefined) {
activeSheet = sheet
studio.setSelection([sheet])
}
break
case 'setSheetObject':
const sheetObj = remote.sheetObjects.get(
`${msg.data.sheet}_${msg.data.key}`,
)
if (sheetObj !== undefined) {
studio.setSelection([sheetObj])
}
break
case 'updateSheetObject':
const sheetObjCB = remote.sheetObjectCBs.get(msg.data.sheetObject)
if (sheetObjCB !== undefined) sheetObjCB(msg.data.values)
break
case 'updateTimeline':
activeSheet = remote.sheets.get(msg.data.sheet)
if (activeSheet !== undefined) {
activeSheet.sequence.position = msg.data.position
}
break
}
})
}
/**
* Editor is visible, this window sends updates
*/
const sendRemote = () => {
studio.ui.restore()
studio.onSelectionChange((value: any[]) => {
if (value.length < 1) return
value.forEach((obj: any) => {
let id = obj.address.sheetId
let type: BroadcastDataEvent = 'setSheet'
let data = {}
switch (obj.type) {
case 'Theatre_Sheet_PublicAPI':
type = 'setSheet'
data = {
sheet: obj.address.sheetId,
}
activeSheet = remote.sheets.get(obj.address.sheetId)
break
case 'Theatre_SheetObject_PublicAPI':
type = 'setSheetObject'
id += `_${obj.address.objectKey}`
data = {
sheet: obj.address.sheetId,
key: obj.address.objectKey,
}
break
}
remote.send({event: type, data: data})
})
})
// Timeline
let position = 0
const onRafUpdate = () => {
if (
activeSheet !== undefined &&
position !== activeSheet.sequence.position
) {
position = activeSheet.sequence.position
const t = activeSheet as ISheet
remote.send({
event: 'updateTimeline',
data: {
position: position,
sheet: t.address.sheetId,
},
})
}
}
const onRaf = () => {
onRafUpdate()
requestAnimationFrame(onRaf)
}
onRafUpdate() // Initial position
onRaf()
}
if (remote.showTheatre) {
sendRemote()
} else {
receiveRemote()
}
}

View file

@ -0,0 +1,55 @@
import React, {useEffect, useRef} from 'react'
import type {CSSProperties} from 'react'
import {types} from '@theatre/core'
import {Box3D} from './Box3D'
import {remote} from './Remote'
// Scene
const SceneCSS: CSSProperties = {
overflow: 'hidden',
position: 'absolute',
left: '0',
right: '0',
top: '0',
bottom: '0',
}
export const Scene: React.FC<{}> = ({}) => {
const containerRef = useRef<HTMLDivElement>(null!)
const sheet = remote.sheet('DOM')
useEffect(() => {
const container = containerRef.current!
const sheetObj = remote.sheetObject(
'DOM',
'Container',
{
perspective: types.number(
Math.max(window.innerWidth, window.innerHeight),
{range: [0, 2000]},
),
originX: types.number(50, {range: [0, 100]}),
originY: types.number(50, {range: [0, 100]}),
},
(values: any) => {
container.style.perspective = `${values.perspective}px`
container.style.perspectiveOrigin = `${values.originX}% ${values.originY}%`
},
)
return () => {
if (sheetObj !== undefined) remote.unsubscribe(sheetObj)
}
}, [])
if (remote.showTheatre) {
SceneCSS.display = 'none'
SceneCSS.visibility = 'hidden'
}
return (
<div ref={containerRef} style={SceneCSS}>
<Box3D sheet={sheet} name="Box" x={100} y={100} />
</div>
)
}

View file

@ -0,0 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom'
import studio from '@theatre/studio'
import {getProject} from '@theatre/core'
import {Scene} from './Scene'
import RemoteController from './RemoteController'
const project = getProject('Sample project')
studio.initialize()
RemoteController(project)
ReactDOM.render(<Scene />, document.getElementById('root'))