-
-
-
+
+
+ {isInspectorEnabled && (
+
-
- )}
-
-
-
-
-
-
-
-
-
-
-
+ )}
+
+
+
+
+
+
+
+
+
+
+
- {isInspectorEnabled && (
- <>
-
-
-
-
-
+
+
+
+
+ >
+ )}
+
+
);
}
diff --git a/src/editor/components/components/Sidebar.js b/src/editor/components/components/Sidebar.js
index 4d460c6a6..69e35daac 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
new file mode 100644
index 000000000..132700781
--- /dev/null
+++ b/src/editor/contexts/TheatreContext.js
@@ -0,0 +1,155 @@
+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 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}
+
+ );
+}