зеркало из https://github.com/mozilla/Spoke.git
Merge branch 'master' of github.com:mozillareality/hubs-editor
This commit is contained in:
Коммит
58b9fb894b
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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() {
|
||||
|
|
Загрузка…
Ссылка в новой задаче