This page outlines opportunities for people who want to contribute to Spoke. We welcome external contributions that align with the project's mission around enabling collaboration and communication through shared 3D spaces. You can find information about how to contribute to Hubs and the supporting projects that make up the platform in the main [Hubs Contributor Doc](https://github.com/mozilla/hubs/blob/master/CONTRIBUTING.md).
Contributors are expected to abide by the project's [Code of Conduct](./CODE_OF_CONDUCT.md) and to be respectful of the project and people working on it.
## Spoke Quick Start
-`git clone https://github.com/mozilla/Spoke.git`
-`cd Spoke`
-`yarn install`
-`yarn start`
Then open **https://localhost:9090** (note: HTTPS is required).
When running against a local self-signed cert reticulum server, you'll need to `export NODE_TLS_REJECT_UNAUTHORIZED=0` for publishing to work.
Spoke is a Single Page Web App built primarily with React and ThreeJS. There is a split between the UI code, the Editor code, and the API wrapper code and we will cover the architecture in these three sections.
### UI
The React UI code can be found in the `/src/ui` folder. The entry point to the app ui is `App.js` which contains the React context providers and routes. Components for non-high priority pages are lazily loaded via dynamic imports. This helps keep the bundle size down for initial loads while reducing load time for the most commonly used pages.
The contexts created in `App.js` include the `ApiContext` which provides access to the `Api` class that we will outline in the Api section. There's also the Auth context which at the time of this doc's creation, only provides the `isAuthenticated` property to consumers. Finally, there's the `ThemeProvider` which provides all of the `styled-components` theme variables for the rest of the app.
Most of the pages are pretty self-explanatory in their architecture, but we will go into detail for the project page / `EditorContainer` component and it's dependencies.
The EditorContainer is the root component for the Spoke editor. The EditorContainer component itself is a fairly large class-based component that hasn't yet been migrated to React Hooks. It is responsible for fetching the files needed to load or create a project referenced by the current url. It also has a number of other project related event handlers. Aside from handling the app state for projects, it also creates and manages an instance of the `Editor` class, which is the entry point to all of the ThreeJS editor logic. In the component's render function you can see all of the high level components which create the basic structure of the editor and provide the editor-related React contexts.
The editor UI is then split up into a number of panels and a few other high level components like the Toolbar, Dialogs, and Onboarding.
#### ViewportPanelContainer
This component represents the 3D viewport in the editor. It has to interface with the `Renderer` and `SpokeControls` classes. It also has to handle drag and drop events for various asset types.
#### HierarchyPanelContainer
This component represents the 3D scene graph that is editable to the user. The items in the hierarchy represent user-editable Elements and not the whole ThreeJS scene graph. There is a lot of logic in this component relating to drag and drop events and keyboard shortcuts. It also uses [react-window](https://github.com/bvaughn/react-window) to virtualize component rendering.
#### PropertiesPanelContainer
This component handles rendering the properties for the currently selected element(s). Note that there is currently support for editing multiple selected nodes of the same type, but not different types of nodes. Node editors are rendered for the currently selected node type and these are registered in `config.js`.
#### AssetsPanel
The AssetsPanel renders all registered asset sources and their assets. It has drag and drop and infinite scrolling behaviors for the asset grid. It also uses the asset sources to render a search/filter toolbar for each source.
#### Toolbar
The Toolbar contains a number of input components that interface with the Spoke controls and EditorContainer. It also renders the dropdown menu.
Elements are 3D objects that are surfaced in the Spoke asset panel and visible in the scene hierarchy panel. Their properties can also be edited in the properties panel. They are higher-level instances of ThreeJS Object3Ds.
The terms "node" and "element" can pretty much be used interchangeably in Spoke. "Element" is the term we like to surface to creators and in the code we often use "node".
Creating an element requires three pieces:
- Node Class: A ThreeJS Object3D that uses the Object3DMixin to add serialization, deserialization, export, lifecycle methods and property getters/setters.
- Node Property Editor: A React component that shows up in the Spoke property panel when the element is selected. It contains all the editable properties of a given node.
- Node Registration: All nodes are registered in `config.js`. When registering a node you need to provide the Node Property Editor and the Node Class.
A Node class is just a ThreeJS Object3D that uses the Object3DMixin.
Here's an example of our simplest Node, the `GroupNode`:
```js
import { Group } from "three";
import EditorNodeMixin from "./EditorNodeMixin";
export default class GroupNode extends EditorNodeMixin(Group) {
static componentName = "group";
static nodeName = "Group";
serialize() {
return super.serialize({
group: {}
});
}
}
```
In this example the `EditorNodeMixin` mixes in a number of properties into the `Group` class. We then extend that class and set some required static properties:
`componentName`: This is the key used in serialization/deserialization of Spoke files.
`nodeName`: This is the name of the element used in the Spoke UI.
In the `serialize` method we write out the `group` component so that it is properly saved to the Spoke project. The group component has no additional data so it can be an empty object. The default implementation (Called via `super.serialize`), adds the `transform`, `visible`, and `editor-settings` components for us.
The `EditorNodeMixin` class contains a number of other methods and lifecycle methods that are documented in that file.
In Spoke, node editors define the component shown in the property panel for the currently selected object. The `GroupNodeEditor` implementation is a very simple example of this. It renders a basic `NodeEditor` with just the component name and description. By default, the parent of the `NodeEditor` (`PropertiesPanelContainer`) will render the name field, visibility checkbox, the node enabled/disabled checkbox, and transform fields. Some of these can be enabled/disabled in the node class. For example the transform fields can be disabled by setting `disableTransform` to `true`.
The `iconComponent` and `description` static fields are used in the assets panel, hierarchy panel, and more. The `iconComponent` property should be a React component, preferably from the `styled-icons` library to keep things simple and the bundle small. You could use any custom component class there though. The description field should tell the user what the component does and act as in-editor documentation.
```js
import React from "react";
import NodeEditor from "./NodeEditor";
import { Cubes } from "styled-icons/fa-solid/Cubes";
Another extensible part of Spoke is the assets panel. Spoke comes with a number of asset source for elements, models, kits, and more. You can add your own sources with just a little code.
Asset sources can be found in `/src/ui/assets/sources`. All sources should extend the `BaseAssetSource` class. You can also extend one of the higher level classes.
- ImageMediaSource
- VideoMediaSource
- ModelMediaSource
- KitSource
- AssetManifestSource
Each asset source must define a name, icon, asset panel component, and search function. These higher level classes have some good defaults for these various asset types.
#### Image/Video/Model Media Sources
These sources are relatively similar with the main difference being the `assetPanelComponent` being used.
Media sources are intended to be used with an external API, in our case all of these requests are made through Reticulum's media search API.
The sources can be relatively simple like the `BingImagesSource`:
```js
import ImageMediaSource from "../ImageMediaSource";
export default class BingImagesSource extends ImageMediaSource {
videoUrl: result && result.images && result.images.preview && result.images.preview.url,
label: result.name,
type: ItemTypes.Video,
url: result.url,
nodeClass: VideoNode,
initialProps: {
name: result.name,
src: result.url
}
})),
suggestions,
nextCursor,
hasMore: !!nextCursor
};
}
}
```
You can also add tags, categories, and more as search options. The `SketchfabSource` is a good example to follow for these settings.
#### Kit Source
Spoke comes with a few kits including the Architecture Kit and Rock Kit. These kits are packaged as a single glTF file where all of the pieces have a specific `kit-piece` glTF component.
Defining a kit source is as simple as extending the `KitSource`, giving it a name, id and glTF model source.
To create this glTF asset, you'll want to follow our kit packaging guide.
```js
import KitSource from "../KitSource";
import { TransformPivot } from "../../../editor/controls/SpokeControls";
export default class ArchitectureKitSource extends KitSource {
// Images take a while to load so we set a debounce timeout
this.searchDebounceTimeout = 500;
}
}
```
#### Asset Manifest Source
The asset manifest source fetches an externally hosted asset manifest and lists all of the assets referenced in it. It's a good option if you have a collection of images, videos, sounds, or models and you want to include them as an asset source in spoke.
To create an asset manifest source, extend the `AssetManifestSource` class and pass the name and asset manifest url to the constructor.
An asset manifest is defined with the following structure:
```json
{
// Currently unused, but you should set this to the same value as you pass to the AssetManifestSource constructor.
"name": "Hubs Sound Pack",
// Placeholder trext to show in the asset source's search bar.
"searchPlaceholder": "Search sounds...",
// The assets to show in the assets panel
"assets": [
{
// Must be a unique id
"id": "Meeting_Room_no_Music",
// The text to show in the ui for the asset item
"label": "Meeting Room no Music",
// Used for filtering assets by type, what icon to show, the component to use, etc.
// "Audio", "Image", "Video" or "Model" are currently supported.
"type": "Audio",
// The relative path to the asset
"url": "Meeting_Room/Meeting_Room_no_Music.mp3",
// Tags are used to filter assets. They should be defined in the "tags" section in the manifest. The values in this array should match the values in the "tags" section.
"tags": [
"Full_Mix",
"Meeting_Room"
]
}
// ...
],
// Tags are used to filter assets. The tags hierarchy is shown on the left side of the assets panel.
"tags": [
{
// The text shown in the UI
"label": "Full Mix",
// The value used above in the "assets" section.
"value": "Full_Mix"
},
{
"label": "Meeting Room",
"value": "Meeting_Room",
// Tags can have children that can be expanded/collapsed.
"children": [
{
"label": "Components",
// Note that the value is a path to the tag. This is mostly to avoid collisions between values. All tag values must be unique.
"value": "Meeting_Room/Components"
}
]
}
// ...
]
}
```
Here is an example of the `AssetManifestSource` in use with our Hubs Sound Pack.
```js
import AssetManifestSource from "../AssetManifestSource";
export default class HubsSoundPackSource extends AssetManifestSource {
Our [Hubs Sound Pack repository](https://github.com/MozillaReality/hubs-sound-pack) is a great example and starting point for creating your own asset manifest source.
Note that the repository contains a [script for generating an asset manifest](https://github.com/MozillaReality/hubs-sound-pack/blob/master/scripts/manifest-generator.js) from the repository's folder structure. It's fairly specific for this project, but you could easily adopt it for your own project.