Merge branch 'master' of github.com:mozillareality/hubs-editor

This commit is contained in:
Brian Peiris 2018-06-07 14:41:55 -07:00
Родитель 4030f83cdd 84ebe43e62
Коммит 58b9fb894b
20 изменённых файлов: 267 добавлений и 173 удалений

Просмотреть файл

@ -0,0 +1,43 @@
import React from "react";
import PropTypes from "prop-types";
import { DragSource } from "react-dnd";
import Icon from "../components/Icon";
import fileIcon from "../assets/file-icon.svg";
import folderIcon from "../assets/folder-icon.svg";
import styles from "./DraggableFile.scss";
import iconStyles from "../components/Icon.scss";
function DraggableFile({ file, selected, onClick, connectDragSource }) {
return connectDragSource(
<div className={styles.draggableFile}>
<Icon
key={file.uri}
name={file.name}
src={file.isDirectory ? folderIcon : fileIcon}
selected={selected}
onClick={e => onClick(e, file)}
className={iconStyles.small}
/>
</div>
);
}
DraggableFile.propTypes = {
file: PropTypes.object,
selected: PropTypes.bool,
onClick: PropTypes.func,
connectDragSource: PropTypes.func
};
export default DragSource(
"file",
{
beginDrag({ file }) {
return { file };
}
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
})
)(DraggableFile);

Просмотреть файл

@ -0,0 +1,3 @@
:local(.draggableFile) {
display: flex;
}

Просмотреть файл

@ -0,0 +1,33 @@
import React from "react";
import PropTypes from "prop-types";
import { DropTarget } from "react-dnd";
import styles from "./FileDropTarget.scss";
function FileDropTarget({ connectDropTarget, children }) {
return connectDropTarget(<div className={styles.fileDropTarget}>{children}</div>);
}
FileDropTarget.propTypes = {
connectDropTarget: PropTypes.func,
children: PropTypes.node
};
export default DropTarget(
"file",
{
drop(props, monitor) {
const item = monitor.getItem();
if (props.onDropFile) {
props.onDropFile(item.file);
}
return item;
}
},
(connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
})
)(FileDropTarget);

Просмотреть файл

@ -0,0 +1,3 @@
:local(.fileDropTarget) {
display: flex;
}

25
src/components/Icon.js Normal file
Просмотреть файл

@ -0,0 +1,25 @@
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import styles from "./Icon.scss";
export default function Icon({ name, src, selected, onClick, className }) {
const fullClassName = classNames(styles.icon, className, {
[styles.selected]: selected
});
return (
<div className={fullClassName} onClick={onClick}>
<img className={styles.image} src={src} />
<div className={styles.name}>{name}</div>
</div>
);
}
Icon.propTypes = {
name: PropTypes.string.isRequired,
src: PropTypes.string.isRequired,
selected: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
};

46
src/components/Icon.scss Normal file
Просмотреть файл

@ -0,0 +1,46 @@
@import "../theme";
:local(.icon) {
display: flex;
flex-wrap: nowrap;
flex-direction: column;
padding: 25px;
align-items: center;
width: 180px;
height: 180px;
&:local(.selected) {
background-color: $selected;
}
:local(.image) {
width: 150px;
height: 150px;
}
:local(.name) {
margin-top: 12px;
text-overflow: ellipsis;
word-wrap: break-word;
white-space: nowrap;
width: 100px;
overflow: hidden;
text-align: center;
}
}
:local(.small) {
padding: 8px;
width: 80px;
height: 80px;
:local(.image) {
width: 50px;
height: 50px;
}
:local(.name) {
margin-top: 8px;
font-size: 11px;
}
}

Просмотреть файл

@ -1,36 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import styles from "./IconGrid.scss";
export default function IconGrid({ icons, onSelect, small }) {
const containerClassName = classNames(styles.iconGrid, { [styles.small]: small });
return (
<div className={containerClassName}>
{icons.map(icon => {
const className = classNames(styles.item, { [styles.selected]: icon.selected });
return (
<div key={icon.id} onClick={e => onSelect(icon, e)} className={className}>
<img className={styles.icon} src={icon.src} />
<div className={styles.name}>{icon.name}</div>
</div>
);
})}
</div>
);
export default function IconGrid({ children }) {
return <div className={styles.iconGrid}>{children}</div>;
}
IconGrid.propTypes = {
small: PropTypes.bool,
icons: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
src: PropTypes.string.isRequired,
selected: PropTypes.bool
})
).isRequired,
onSelect: PropTypes.func.isRequired
children: PropTypes.arrayOf(PropTypes.element)
};

Просмотреть файл

@ -1,41 +1,6 @@
@import "../theme";
:local(.iconGrid) {
display: flex;
flex: 1;
:local(.item) {
width: 200px;
height: 200px;
display: flex;
flex-wrap: wrap;
flex-direction: column;
padding: 25px;
align-items: center;
justify-content: space-between;
&:local(.selected) {
background-color: $selected;
}
:local(.icon) {
width: 175px;
height: 175px;
}
}
&:local(.small) {
:local(.item) {
width: 120px;
height: 120px;
padding: 12px;
}
:local(.icon) {
width: 100px;
height: 100px;
}
}
}

Просмотреть файл

@ -13,5 +13,5 @@ export default function InputGroup({ name, children }) {
InputGroup.propTypes = {
name: PropTypes.string,
children: PropTypes.arrayOf(PropTypes.element)
children: PropTypes.any
};

Просмотреть файл

@ -3,40 +3,30 @@ import PropTypes from "prop-types";
import styles from "./ProjectModal.scss";
import Header from "./Header";
import IconGrid from "./IconGrid";
import Icon from "./Icon";
import TabNavigation from "./TabNavigation";
import Tab from "./Tab";
import NativeFileInput from "./NativeFileInput";
import defaultThumbnail from "../assets/default-thumbnail.png";
function onSelectIcon(icon, event, projects, onSelectProject) {
const project = projects.find(({ uri }) => uri === icon.id);
onSelectProject(project, event);
}
export default function ProjectModal({ tab, projects, onSelectProject, onChangeTab, onOpenProject }) {
const tabs = [
{
name: "Recent Projects",
selected: tab === "projects",
onClick: () => onChangeTab("projects")
},
{
name: "Templates",
selected: tab === "templates",
onClick: () => onChangeTab("templates")
}
];
const icons = projects.map(project => ({
id: project.uri,
src: project.icon || defaultThumbnail,
name: project.name
}));
return (
<div className={styles.projectModal}>
<Header title="Projects" />
<TabNavigation tabs={tabs} />
<IconGrid icons={icons} onSelect={(icon, event) => onSelectIcon(icon, event, projects, onSelectProject)} />
<TabNavigation>
<Tab name="Recent Projects" selected={tab === "projects"} onClick={() => onChangeTab("projects")} />
<Tab name="Templates" selected={tab === "templates"} onClick={() => onChangeTab("templates")} />
</TabNavigation>
<IconGrid>
{projects.map(project => (
<Icon
key={project.uri}
src={project.icon || defaultThumbnail}
name={project.name}
onClick={() => onSelectProject(project)}
/>
))}
</IconGrid>
<NativeFileInput label="Open Project..." onChange={onOpenProject} />
</div>
);

22
src/components/Tab.js Normal file
Просмотреть файл

@ -0,0 +1,22 @@
import React from "react";
import PropTypes from "prop-types";
import styles from "./Tab.scss";
import classNames from "classnames";
export default function Tab({ name, selected, onClick }) {
const className = classNames(styles.tab, {
[styles.selected]: selected
});
return (
<div className={className} onClick={onClick}>
{name}
</div>
);
}
Tab.propTypes = {
name: PropTypes.string,
selected: PropTypes.bool,
onClick: PropTypes.func
};

26
src/components/Tab.scss Normal file
Просмотреть файл

@ -0,0 +1,26 @@
@import "../theme";
:local(.tab) {
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
padding: 0 8px;
background-color: $background;
border: 1px solid $border;
cursor: pointer;
&:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
&:local(.selected) {
background-color: $selected;
}
}

Просмотреть файл

@ -1,44 +1,11 @@
import React from "react";
import PropTypes from "prop-types";
import styles from "./TabNavigation.scss";
import classNames from "classnames";
function Tab({ children, selected, onClick }) {
const className = classNames(styles.tab, {
[styles.selected]: selected
});
return (
<div className={className} onClick={onClick}>
{children}
</div>
);
}
Tab.propTypes = {
children: PropTypes.string,
selected: PropTypes.bool,
onClick: PropTypes.func
};
export default function TabNavigation({ tabs }) {
return (
<div className={styles.tabNavigation}>
{tabs.map(({ selected, name, onClick }) => (
<Tab key={name} selected={selected} onClick={onClick}>
{name}
</Tab>
))}
</div>
);
export default function TabNavigation({ children }) {
return <div className={styles.tabNavigation}>{children}</div>;
}
TabNavigation.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({
selected: PropTypes.bool,
onClick: PropTypes.func,
name: PropTypes.string
})
)
children: PropTypes.arrayOf(PropTypes.element)
};

Просмотреть файл

@ -1,32 +1,6 @@
@import "../theme";
:local(.tabNavigation) {
display: flex;
justify-content: center;
height: 24px;
}
:local(.tab) {
display: flex;
align-items: center;
justify-content: center;
min-width: 100px;
padding: 0 8px;
background-color: $background;
border: 1px solid $border;
cursor: pointer;
&:first-child {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
&:local(.selected) {
background-color: $selected;
}
}

Просмотреть файл

@ -5,10 +5,9 @@ import "../vendor/react-ui-tree/index.scss";
import classNames from "classnames";
import { withProject } from "./ProjectContext";
import IconGrid from "../components/IconGrid";
import fileIcon from "../assets/file-icon.svg";
import folderIcon from "../assets/folder-icon.svg";
import { openFile } from "../api";
import styles from "./AssetExplorerPanelContainer.scss";
import DraggableFile from "../components/DraggableFile";
class AssetExplorerPanelContainer extends Component {
static propTypes = {
@ -70,7 +69,7 @@ class AssetExplorerPanelContainer extends Component {
});
};
onSelectIcon = ({ file }) => {
onClickFile = (e, file) => {
if (this.state.singleClickedFile && file.uri === this.state.singleClickedFile.uri) {
if (file.isDirectory) {
this.setState({ selectedDirectory: file });
@ -117,13 +116,6 @@ class AssetExplorerPanelContainer extends Component {
const selectedDirectory = this.state.selectedDirectory || this.state.tree;
const files = selectedDirectory.files || [];
const selectedFile = this.state.selectedFile;
const icons = files.map(file => ({
id: file.uri,
name: file.name,
src: file.isDirectory ? folderIcon : fileIcon,
selected: selectedFile && selectedFile.uri === file.uri,
file
}));
return (
<div className={styles.assetExplorerPanelContainer}>
@ -138,7 +130,16 @@ class AssetExplorerPanelContainer extends Component {
/>
</div>
<div className={styles.rightColumn}>
<IconGrid icons={icons} onSelect={this.onSelectIcon} small />
<IconGrid>
{files.map(file => (
<DraggableFile
key={file.uri}
file={file}
selected={selectedFile && selectedFile.uri === file.uri}
onClick={this.onClickFile}
/>
))}
</IconGrid>
</div>
</div>
);

Просмотреть файл

@ -1,7 +1,7 @@
:local(.assetExplorerPanelContainer) {
display: flex;
flex-direction: row;
height: 100%;
flex: 1;
}
:local(.leftColumn) {

Просмотреть файл

@ -1,5 +1,7 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { DragDropContextProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import Editor from "../components/Editor";
import ProjectModalContainer from "./ProjectModalContainer";
import NewProjectModalContainer from "./NewProjectModalContainer";
@ -172,13 +174,15 @@ class EditorContainer extends Component {
return (
<ProjectProvider value={projectContext}>
<Editor
initialPanels={this.props.initialPanels}
renderPanel={this.renderPanel}
openModal={this.state.openModal}
onCloseModal={this.onCloseModal}
onPanelChange={this.onPanelChange}
/>
<DragDropContextProvider backend={HTML5Backend}>
<Editor
initialPanels={this.props.initialPanels}
renderPanel={this.renderPanel}
openModal={this.state.openModal}
onCloseModal={this.onCloseModal}
onPanelChange={this.onPanelChange}
/>
</DragDropContextProvider>
</ProjectProvider>
);
}

Просмотреть файл

@ -3,6 +3,7 @@ import PropTypes from "prop-types";
import Viewport from "../components/Viewport";
import { withEditor } from "./EditorContext";
import styles from "./ViewportPanelContainer.scss";
import FileDropTarget from "../components/FileDropTarget";
class ViewportPanelContainer extends Component {
static propTypes = {
@ -19,10 +20,18 @@ class ViewportPanelContainer extends Component {
this.props.editor.createRenderer(this.canvasRef.current);
}
onDropFile = file => {
if (file.ext === "gltf") {
this.props.editor.loadGLTF(file.uri);
}
};
render() {
return (
<div className={styles.viewportPanelContainer}>
<Viewport ref={this.canvasRef} />
<FileDropTarget onDropFile={this.onDropFile}>
<Viewport ref={this.canvasRef} onDropFile={this.onDropFile} />
</FileDropTarget>
</div>
);
}

Просмотреть файл

@ -1,4 +1,4 @@
:local(.viewportPanelContainer) {
display: flex;
flex: 1;
}
}

Просмотреть файл

@ -97,6 +97,8 @@ export default class Editor {
this.helpers = {};
this.viewport = null;
this.gltfLoader = new THREE.GLTFLoader();
}
onWindowResize = () => {
@ -226,6 +228,12 @@ export default class Editor {
this.textures[texture.uuid] = texture;
}
loadGLTF(url) {
this.gltfLoader.load(url, ({ scene }) => {
this.addObject(scene);
});
}
//
addHelper = (function() {