From cd6f0a688484c532f0ef9b096432f14b0409b356 Mon Sep 17 00:00:00 2001 From: Kieran Farr Date: Fri, 20 Dec 2024 22:03:37 -0800 Subject: [PATCH 1/2] barely working theatre v1 --- package-lock.json | 29 ++++++++ package.json | 2 + src/editor/components/Main.js | 103 +++++++++++++------------- src/editor/contexts/TheatreContext.js | 26 +++++++ 4 files changed, 110 insertions(+), 50 deletions(-) create mode 100644 src/editor/contexts/TheatreContext.js diff --git a/package-lock.json b/package-lock.json index 7cbdadb7..b1152f3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@mux/mux-player-react": "^2.9.1", "@react-google-maps/api": "^2.19.3", "@stripe/stripe-js": "^3.4.1", + "@theatre/core": "^0.7.2", + "@theatre/studio": "^0.7.2", "aframe-atlas-uvs-component": "^3.0.0", "aframe-cursor-teleport-component": "^1.6.0", "aframe-extras": "^7.5.1", @@ -7726,6 +7728,33 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@theatre/core": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@theatre/core/-/core-0.7.2.tgz", + "integrity": "sha512-IDQa/6WY7mIJAtsSd4EgNcM0IUZkl+FrqZ8DdYiCVTFap9ARDNmrngJOeFjJOsnnaHlc5GdEB/jj7fsjbIrAzQ==", + "dependencies": { + "@theatre/dataverse": "0.7.2" + } + }, + "node_modules/@theatre/dataverse": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@theatre/dataverse/-/dataverse-0.7.2.tgz", + "integrity": "sha512-YyfoyX7EyhFUY2OM5fsM0LPrs1SdgLwpiTMkkvTIoZLdOwvQhstjYq4Yz/8ZncJlRoTWvakfmgvCaBN+QuBYxg==", + "dependencies": { + "lodash-es": "^4.17.21" + } + }, + "node_modules/@theatre/studio": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@theatre/studio/-/studio-0.7.2.tgz", + "integrity": "sha512-p6LTKzJWVlcHkpGzIlNHh9AkGbB3E+0q9Pjxv+OJoTDe1IK+CMKW695Wp+1//lB4vfC9qShe4z/p+Zaj1q8KtA==", + "dependencies": { + "@theatre/dataverse": "0.7.2" + }, + "peerDependencies": { + "@theatre/core": "*" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", diff --git a/package.json b/package.json index 970a7865..87ac264c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@mux/mux-player-react": "^2.9.1", "@react-google-maps/api": "^2.19.3", "@stripe/stripe-js": "^3.4.1", + "@theatre/core": "^0.7.2", + "@theatre/studio": "^0.7.2", "aframe-atlas-uvs-component": "^3.0.0", "aframe-cursor-teleport-component": "^1.6.0", "aframe-extras": "^7.5.1", diff --git a/src/editor/components/Main.js b/src/editor/components/Main.js index 3a80e89a..5c8fea6e 100644 --- a/src/editor/components/Main.js +++ b/src/editor/components/Main.js @@ -20,6 +20,7 @@ import { IntroModal } from './modals/IntroModal'; import { NewModal } from './modals/NewModal'; import { ToolbarWrapper } from './scenegraph/ToolbarWrapper.js'; import useStore from '@/store'; +import { TheatreProvider } from '../contexts/TheatreContext'; THREE.ImageUtils.crossOrigin = ''; @@ -108,59 +109,61 @@ export default function Main() { const isInspectorEnabled = useStore((state) => state.isInspectorEnabled); return ( -
- - {isInspectorEnabled && ( -
- -
- +
+ + {isInspectorEnabled && ( +
+ +
+ +
-
- )} - - - - - - - - - - - + )} + + + + + + + + + + + - {isInspectorEnabled && ( - <> -
- -
-
- -
-
- - -
- + {isInspectorEnabled && ( + <> +
+
-
- - )} -
+
+ +
+
+ + +
+ +
+
+ + )} +
+ ); } diff --git a/src/editor/contexts/TheatreContext.js b/src/editor/contexts/TheatreContext.js new file mode 100644 index 00000000..8c797654 --- /dev/null +++ b/src/editor/contexts/TheatreContext.js @@ -0,0 +1,26 @@ +import { createContext, useEffect } from 'react'; +import { getProject } from '@theatre/core'; +import studio from '@theatre/studio'; + +export const TheatreContext = createContext(); + +export function TheatreProvider({ children }) { + useEffect(() => { + // Initialize Theatre.js + studio.initialize(); + + // Create a project + const project = getProject('3DStreet Animation'); + const sheet = project.sheet('Main Sheet'); + console.log('[theatre] project', project); + console.log('[theatre] sheet', sheet); + + return () => { + // Cleanup if needed + }; + }, []); + + return ( + {children} + ); +} From 24208189298ae3b9c8fd24d1f661feb0d0f48105 Mon Sep 17 00:00:00 2001 From: Kieran Farr Date: Sat, 21 Dec 2024 19:44:37 -0800 Subject: [PATCH 2/2] barely working theatre --- src/editor/components/components/Sidebar.js | 320 ++++++++++---------- src/editor/contexts/TheatreContext.js | 141 ++++++++- 2 files changed, 297 insertions(+), 164 deletions(-) diff --git a/src/editor/components/components/Sidebar.js b/src/editor/components/components/Sidebar.js index 4d460c6a..69e35daa 100644 --- a/src/editor/components/components/Sidebar.js +++ b/src/editor/components/components/Sidebar.js @@ -8,7 +8,7 @@ import ComponentsContainer from './ComponentsContainer'; import Events from '../../lib/Events'; import Mixins from './Mixins'; import PropTypes from 'prop-types'; -import React from 'react'; +import { useEffect, useState } from 'react'; import capitalize from 'lodash-es/capitalize'; import classnames from 'classnames'; import { @@ -17,181 +17,185 @@ import { SegmentIcon, ManagedStreetIcon } from '../../icons'; -import GeoSidebar from './GeoSidebar'; // Make sure to create and import this new component +import GeoSidebar from './GeoSidebar'; import IntersectionSidebar from './IntersectionSidebar'; import StreetSegmentSidebar from './StreetSegmentSidebar'; import ManagedStreetSidebar from './ManagedStreetSidebar'; import AdvancedComponents from './AdvancedComponents'; -export default class Sidebar extends React.Component { - static propTypes = { - entity: PropTypes.object, - visible: PropTypes.bool - }; +import { useTheatre } from '../../contexts/TheatreContext'; + +export default function Sidebar({ entity, visible }) { + const [showSideBar, setShowSideBar] = useState(true); + const { addEntityToTheatre, controlledEntities } = useTheatre(); - constructor(props) { - super(props); - this.state = { - showSideBar: true + useEffect(() => { + const onEntityUpdate = (detail) => { + if (detail.entity !== entity) { + return; + } + if ( + detail.component === 'mixin' || + detail.component === 'data-layer-name' + ) { + // Force update happens automatically in functional components + } }; - } - onEntityUpdate = (detail) => { - if (detail.entity !== this.props.entity) { - return; - } - if ( - detail.component === 'mixin' || - detail.component === 'data-layer-name' - ) { - this.forceUpdate(); - } - }; + const onComponentRemove = (detail) => { + if (detail.entity !== entity) { + return; + } + // Force update happens automatically + }; - onComponentRemove = (detail) => { - if (detail.entity !== this.props.entity) { - return; - } - this.forceUpdate(); - }; + const onComponentAdd = (detail) => { + if (detail.entity !== entity) { + return; + } + // Force update happens automatically + }; - onComponentAdd = (detail) => { - if (detail.entity !== this.props.entity) { - return; - } - this.forceUpdate(); + Events.on('entityupdate', onEntityUpdate); + Events.on('componentremove', onComponentRemove); + Events.on('componentadd', onComponentAdd); + + return () => { + Events.off('entityupdate', onEntityUpdate); + Events.off('componentremove', onComponentRemove); + Events.off('componentadd', onComponentAdd); + }; + }, [entity]); + + const toggleRightBar = () => { + setShowSideBar(!showSideBar); }; - componentDidMount() { - Events.on('entityupdate', this.onEntityUpdate); - Events.on('componentremove', this.onComponentRemove); - Events.on('componentadd', this.onComponentAdd); + if (!entity || !visible) { + return
; } - componentWillUnmount() { - Events.off('entityupdate', this.onEntityUpdate); - Events.off('componentremove', this.onComponentRemove); - Events.off('componentadd', this.onComponentAdd); - } + const entityName = entity.getDOMAttribute('data-layer-name'); + const entityMixin = entity.getDOMAttribute('mixin'); + const formattedMixin = entityMixin + ? capitalize(entityMixin.replaceAll('-', ' ').replaceAll('_', ' ')) + : null; - // additional toggle for hide/show panel by clicking the button - toggleRightBar = () => { - this.setState({ showSideBar: !this.state.showSideBar }); - }; + const className = classnames({ + outliner: true, + hide: showSideBar, + 'mt-16': true + }); - render() { - const entity = this.props.entity; - const visible = this.props.visible; - const className = classnames({ - outliner: true, - hide: this.state.showSideBar, - 'mt-16': true - }); - if (entity && visible) { - const entityName = entity.getDOMAttribute('data-layer-name'); - const entityMixin = entity.getDOMAttribute('mixin'); - const formattedMixin = entityMixin - ? capitalize(entityMixin.replaceAll('-', ' ').replaceAll('_', ' ')) - : null; - return ( -
- {this.state.showSideBar ? ( - <> -
-
- {entity.getAttribute('managed-street') ? ( - - ) : entity.getAttribute('street-segment') ? ( - - ) : ( - - )} - {entityName || formattedMixin} -
-
- -
-
-
- {entity.id !== 'reference-layers' && - !entity.getAttribute('street-segment') ? ( - <> - {!!entity.mixinEls.length && } - {entity.hasAttribute('data-no-transform') ? ( - <> - ) : ( - - )} - {entity.getAttribute('intersection') && ( - - )} - {entity.getAttribute('managed-street') && ( - - )} - - + return ( +
+ {showSideBar ? ( + <> +
+
+ {entity.getAttribute('managed-street') ? ( + + ) : entity.getAttribute('street-segment') ? ( + + ) : ( + + )} + {entityName || formattedMixin} +
+
+ +
+
+
+ {entity.id !== 'reference-layers' && + !entity.getAttribute('street-segment') ? ( + <> + {!!entity.mixinEls.length && } + {entity.hasAttribute('data-no-transform') ? ( + <> ) : ( + + )} + {entity.getAttribute('intersection') && ( + + )} + {entity.getAttribute('managed-street') && ( + + )} + + + ) : ( + <> + {entity.getAttribute('street-segment') && ( <> - {entity.getAttribute('street-segment') && ( - <> - -
- -
- - )} - {entity.id === 'reference-layers' && ( - - )} + +
+ +
)} + {entity.id === 'reference-layers' && ( + + )} + + )} +
+ + ) : ( + <> +
+
+ + {entityName || formattedMixin} + +
+ {entity.getAttribute('managed-street') ? ( + + ) : entity.getAttribute('street-segment') ? ( + + ) : ( + + )}
- - ) : ( - <> -
-
- - {entityName || formattedMixin} - -
- {entity.getAttribute('managed-street') ? ( - - ) : entity.getAttribute('street-segment') ? ( - - ) : ( - - )} -
-
-
- - )} -
- ); - } else { - return
; - } - } +
+
+ + )} +
+ ); } + +Sidebar.propTypes = { + entity: PropTypes.object, + visible: PropTypes.bool +}; diff --git a/src/editor/contexts/TheatreContext.js b/src/editor/contexts/TheatreContext.js index 8c797654..13270078 100644 --- a/src/editor/contexts/TheatreContext.js +++ b/src/editor/contexts/TheatreContext.js @@ -1,26 +1,155 @@ -import { createContext, useEffect } from 'react'; +import { createContext, useContext, useEffect, useState } from 'react'; import { getProject } from '@theatre/core'; import studio from '@theatre/studio'; export const TheatreContext = createContext(); +export function useTheatre() { + return useContext(TheatreContext); +} + export function TheatreProvider({ children }) { + const [project, setProject] = useState(null); + const [sheet, setSheet] = useState(null); + const [controlledEntities, setControlledEntities] = useState(new Set()); + useEffect(() => { // Initialize Theatre.js studio.initialize(); // Create a project - const project = getProject('3DStreet Animation'); - const sheet = project.sheet('Main Sheet'); - console.log('[theatre] project', project); - console.log('[theatre] sheet', sheet); + const proj = getProject('3DStreet Animation'); + const mainSheet = proj.sheet('Main Sheet'); + + setProject(proj); + setSheet(mainSheet); + + console.log('[theatre] project', proj); + console.log('[theatre] sheet', mainSheet); return () => { // Cleanup if needed }; }, []); + const createValidObjectId = (entity) => { + // Get entity name or mixin as base + const baseName = + entity.getDOMAttribute('data-layer-name') || + entity.getDOMAttribute('mixin') || + 'unnamed'; + + // Clean up the name to be valid for Theatre.js + const cleanName = baseName + .replace(/[^a-zA-Z0-9]/g, '_') // Replace non-alphanumeric with underscore + .replace(/^[^a-zA-Z]/, 'obj_$&') // Ensure starts with letter + .replace(/_{2,}/g, '_'); // Remove duplicate underscores + + // Add a unique suffix using timestamp + const uniqueName = `${cleanName}_${Date.now()}`; + + console.log( + '[theatre] Created object ID:', + uniqueName, + 'for entity:', + entity + ); + return uniqueName; + }; + + const addEntityToTheatre = (entity) => { + if (!sheet || !entity) { + console.warn('[theatre] Cannot add entity - sheet or entity missing', { + sheet, + entity + }); + return; + } + + if (controlledEntities.has(entity.id)) { + console.log('[theatre] Entity already controlled:', entity.id); + return; + } + + console.log('[theatre] Adding entity to control:', entity); + + // Generate valid object ID + const objectId = createValidObjectId(entity); + + try { + // Get current transform values + const position = entity.object3D.position; + const rotation = entity.object3D.rotation; + const material = entity.getAttribute('material'); + + // Create a new object for the entity + const entityObj = sheet.object(objectId, { + position: { + x: position.x, + y: position.y, + z: position.z + }, + rotation: { + x: rotation.x, + y: rotation.y, + z: rotation.z + }, + ...(material ? { opacity: material.opacity || 1 } : {}) + }); + + console.log('[theatre] Created object:', objectId, entityObj); + + // Subscribe to changes + entityObj.onValuesChange((values) => { + const { position, rotation, opacity } = values; + + // Update position + if (position) { + entity.object3D.position.set(position.x, position.y, position.z); + } + + // Update rotation + if (rotation) { + entity.object3D.rotation.set(rotation.x, rotation.y, rotation.z); + } + + // Update opacity if material exists + if (opacity !== undefined && entity.getAttribute('material')) { + entity.setAttribute('material', 'opacity', opacity); + } + }); + + // Add to controlled entities set + setControlledEntities((prev) => new Set(prev).add(entity.id)); + + console.log('[theatre] Successfully added entity to control'); + } catch (error) { + console.error('[theatre] Error adding entity to control:', error); + } + }; + + const removeEntityFromTheatre = (entityId) => { + if (!sheet || !controlledEntities.has(entityId)) return; + + // Remove object from sheet + setControlledEntities((prev) => { + const next = new Set(prev); + next.delete(entityId); + return next; + }); + }; + return ( - {children} + + {children} + ); }