export and import project as a bundle

This commit is contained in:
Val Erastov 2019-02-24 14:43:19 -08:00
parent a1e2b80948
commit c47d53cd86
11 changed files with 276 additions and 26 deletions

View file

@ -57,7 +57,26 @@ export default [
invoke: (context) => context.services.export.imagePng() invoke: (context) => context.services.export.imagePng()
}, },
{
id: 'NativeFormatExport',
appearance: {
cssIcons: ['book'],
label: 'Download Bundle',
info: 'export model and its sketches as a json bundle',
},
invoke: (context) => context.services.export.nativeFormat()
},
{
id: 'NativeFormatImport',
appearance: {
cssIcons: ['book'],
label: 'Import Bundle',
info: 'import native format json(model and its sketches)',
},
invoke: (context) => context.services.projectManager.importProject()
},
{ {
id: 'RefreshSketches', id: 'RefreshSketches',
appearance: { appearance: {

View file

@ -8,13 +8,19 @@ import ToolButton from 'ui/components/ToolButton';
@connect(state => state.ui.floatViews.map(views => ({views}))) @connect(state => state.ui.floatViews.map(views => ({views})))
@mapContext(ctx => ({ @mapContext(ctx => ({
getDescriptor: ctx.services.ui.getFloatView getDescriptor: ctx.services.ui.getFloatView,
initialView: ctx.services.project.hints.FloatView || null
})) }))
export default class FloatView extends React.Component { export default class FloatView extends React.Component {
state = {
selected: null constructor(props) {
}; super();
this.state = {
selected: props.initialView
};
}
render() { render() {
let {views, getDescriptor} = this.props; let {views, getDescriptor} = this.props;
@ -22,7 +28,7 @@ export default class FloatView extends React.Component {
function view(id) { function view(id) {
let {title, icon, Component} = getDescriptor(id); let {title, icon, Component} = getDescriptor(id);
return <Folder className={ls.folder} title={<span> <Fa fw icon={icon}/> {title}</span>}> return <Folder className={ls.folder} title={<span> <Fa fw icon={icon}/> {title}</span>}>
<Component/> <div className={ls.folderContent}><Component/></div>
</Folder>; </Folder>;
} }
@ -31,18 +37,19 @@ export default class FloatView extends React.Component {
return <Icon /> return <Icon />
} }
let selected = this.state.selected;
return <div className={ls.root}> return <div className={ls.root}>
<div className={ls.tabs}> <div className={ls.tabs}>
{views.map(tabId => <ToolButton pressed={this.state.selected === tabId} {views.map(tabId => <ToolButton pressed={selected === tabId}
key={tabId} key={tabId}
onClick={() => this.setState({selected: this.state.selected === tabId ? null : tabId})}> onClick={() => this.setState({selected: selected === tabId ? null : tabId})}>
{<Fa fw icon={getDescriptor(tabId).icon}/>} {<Fa fw icon={getDescriptor(tabId).icon}/>}
</ToolButton>)} </ToolButton>)}
</div> </div>
{this.state.selected && <div className={ls.main}> {selected && view(selected)}
{view(this.state.selected)}
</div>}
</div>; </div>;
} }
} }

View file

@ -16,16 +16,19 @@
font-size: 13px; font-size: 13px;
} }
.main {
display: flex;
flex-direction: column;
width: 235px;
flex: 1;
overflow-y: scroll;
}
.folder { .folder {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
}
.folderContent {
flex: 1;
display: flex;
width: 235px;
overflow: auto;
}
.folderContent > * {
flex: 1;
} }

View file

@ -33,7 +33,11 @@ export function activate(ctx) {
ctx.services.viewer.sceneSetup.render(); ctx.services.viewer.sceneSetup.render();
} }
function nativeFormat() {
ctx.services.projectManager.exportProject(ctx.services.project.id);
}
ctx.services.export = { ctx.services.export = {
stlAscii, imagePng, toStlAsciiString stlAscii, imagePng, toStlAsciiString, nativeFormat
}; };
} }

View file

@ -19,6 +19,7 @@ import * as CraftPlugin from '../craft/craftPlugin';
import * as CraftUiPlugin from '../craft/craftUiPlugin'; import * as CraftUiPlugin from '../craft/craftUiPlugin';
import * as StoragePlugin from '../storagePlugin'; import * as StoragePlugin from '../storagePlugin';
import * as ProjectPlugin from '../projectPlugin'; import * as ProjectPlugin from '../projectPlugin';
import * as ProjectManagerPlugin from '../projectManager/projectManagerPlugin';
import * as SketcherPlugin from '../sketch/sketcherPlugin'; import * as SketcherPlugin from '../sketch/sketcherPlugin';
import * as ExportPlugin from '../exportPlugin'; import * as ExportPlugin from '../exportPlugin';
import * as TpiPlugin from '../tpi/tpiPlugin'; import * as TpiPlugin from '../tpi/tpiPlugin';
@ -54,7 +55,8 @@ export default function startApplication(callback) {
CadRegistryPlugin, CadRegistryPlugin,
ExportPlugin, ExportPlugin,
TpiPlugin, TpiPlugin,
E0Plugin E0Plugin,
ProjectManagerPlugin
]; ];
let plugins = [ let plugins = [

View file

@ -0,0 +1,43 @@
import React from 'react';
import mapContext from 'ui/mapContext';
import {Section} from 'ui/components/Section';
import Fa from 'ui/components/Fa';
import ls from './ProjectManager.less';
import {ContextMenu, ContextMenuItem} from 'ui/components/Menu';
import cmn from 'ui/styles/common.less';
import Button from '../../../../modules/ui/components/controls/Button';
import Folder from '../../../../modules/ui/components/Folder';
@mapContext(ctx => ({
projectManager: ctx.services.projectManager
}))
export class ProjectManager extends React.Component {
render() {
let {projectManager} = this.props;
let projects = projectManager.listProjects();
return <div className={ls.root}>
<Button className={ls.btn}><Fa fw icon='download' /> Download Project</Button>
<Button className={ls.btn}><Fa fw icon='upload' /> Import Project</Button>
<Folder title='Project List'>
<div className={cmn.scrollable}>
{projects.map(p => <Section key={p.id}
label={<ContextMenu items={
<React.Fragment>
<ContextMenuItem label='Download' icon={<Fa fw icon='download' />} onClick={() => alert(1)}/>
<ContextMenuItem label='Clone' icon={<Fa fw icon='copy' />} onClick={() => alert(1)}/>
<ContextMenuItem label='Delete' icon={<Fa className={cmn.dangerColor} fw icon='remove' />} onClick={() => alert(1)}/>
</React.Fragment>
}>
<a href={'?' + p.id} target="_blank"><Fa icon='file'/> {p.id}</a>
</ContextMenu>}>
{p.sketches.length && <Section label={<span><Fa icon='image'/> Sketches</span>} defaultOpen={true}>
{p.sketches.map(sketch => <Section key={sketch} label={sketch}/>)}
</Section>}
</Section>)}
</div>
</Folder>
</div>
}
}

View file

@ -0,0 +1,9 @@
.root a {
text-decoration: none;
}
.btn {
width: 90%;
margin: 2px;
display: block;
}

View file

@ -0,0 +1,148 @@
import {PROJECTS_PREFIX, SKETCH_SUFFIX} from '../projectPlugin';
import {ProjectManager} from './ProjectManager';
import exportTextData from '../../../../modules/gems/exportTextData';
export function activate(ctx) {
function importProject() {
// let uploader = document.getElementById("uploader");
let uploader = document.createElement('input');
uploader.setAttribute('type', 'file');
uploader.style.display = 'none';
document.body.appendChild(uploader);
uploader.click();
function read() {
let reader = new FileReader();
reader.onload = () => {
try {
let bundle = JSON.parse(reader.result);
let promptedId = uploader.value;
let iof = promptedId.search(/([^/\\]+)$/);
if (iof !== -1) {
promptedId = promptedId.substring(iof).replace(/\.json$/, '');
}
let projectId = prompt("New Project Name", promptedId);
if (projectId && !checkExistence(projectId)) {
let sketchesNamespace = PROJECTS_PREFIX + projectId + SKETCH_SUFFIX;
ctx.services.storage.set(PROJECTS_PREFIX + projectId, JSON.stringify(bundle.model));
bundle.sketches.forEach(s => ctx.services.storage.set(sketchesNamespace + s.id, JSON.stringify(s.data)));
openProject(projectId);
}
} finally {
document.body.removeChild(uploader);
}
};
reader.readAsText(uploader.files[0]);
}
uploader.addEventListener('change', read, false);
}
function exportProject(id) {
let modelData = ctx.services.storage.get(PROJECTS_PREFIX + id);
if (modelData) {
let data = '{\n"model":\n' + modelData + ',\n "sketches": [\n';
let sketchesNamespace = PROJECTS_PREFIX + id + SKETCH_SUFFIX;
let sketchKeys = ctx.services.storage.getAllKeysFromNamespace(sketchesNamespace);
data += sketchKeys.map(key => `{"id":"${key.substring(sketchesNamespace.length)}", "data": `
+ ctx.services.storage.get(key) + "}").join('\n,');
data += '\n]\n}';
exportTextData(data, id + '.json');
}
}
function checkExistence(projectId) {
if (exists(projectId)) {
alert('Project with name ' + projectId + ' already exists');
return true;
}
return false;
}
function cloneProject(oldId, newId) {
if (checkExistence(newId)) {
return
}
let data = ctx.services.storage.get(PROJECTS_PREFIX + oldId);
if (data) {
ctx.services.storage.set(PROJECTS_PREFIX + newId);
let sketchesNamespace = PROJECTS_PREFIX + oldId + SKETCH_SUFFIX;
let sketchKeys = ctx.services.storage.getAllKeysFromNamespace(sketchesNamespace);
sketchKeys.forEach(key => {
ctx.services.storage.set(PROJECTS_PREFIX + newId + SKETCH_SUFFIX + key.substring(sketchesNamespace.length), ctx.services.storage.get(key));
});
}
}
function exists(projectId) {
return ctx.services.storage.exists(PROJECTS_PREFIX + projectId);
}
function listProjects() {
let allProjectKeys = ctx.services.storage.getAllKeysFromNamespace(PROJECTS_PREFIX);
let projects = {};
function getProject(id) {
let project = projects[id];
if (!project) {
project = {
id,
sketches: []
};
projects[id] = project;
}
return project;
}
for(let key of allProjectKeys) {
let sketchSuffixIdx = key.indexOf(SKETCH_SUFFIX);
if (sketchSuffixIdx !== -1) {
let projectId = key.substring(PROJECTS_PREFIX.length, sketchSuffixIdx);
let sketchId = key.substring(sketchSuffixIdx + SKETCH_SUFFIX.length);
getProject(projectId).sketches.push(sketchId);
} else {
let projectId = key.substring(PROJECTS_PREFIX.length);
getProject(projectId)
}
}
return Object.keys(projects).sort().map(key => projects[key]);
}
function deleteProject(projectId) {
if (confirm(`Project ${projectId} will be deleted. Continue?`)) {
ctx.services.storage.remove(PROJECTS_PREFIX + projectId);
}
}
function renameProject(oldProjectId, newProjectId) {
if (checkExistence(newProjectId)) {
return
}
cloneProject(oldProjectId, newProjectId);
deleteProject(oldProjectId);
}
function newProject(newProjectId) {
if (checkExistence(newProjectId)) {
return
}
openProject(newProjectId);
}
function openProject(projectId) {
window.open('?' + projectId);
}
ctx.services.ui.registerFloatView('ProjectManager', ProjectManager, 'Project Manager', 'book');
ctx.services.projectManager = {
listProjects, openProject, newProject, renameProject, deleteProject,
exists, cloneProject, exportProject, importProject
}
}

View file

@ -3,7 +3,8 @@ import {runSandbox} from './sandbox';
import {LOG_FLAGS} from './logFlags'; import {LOG_FLAGS} from './logFlags';
export const STORAGE_GLOBAL_PREFIX = 'TCAD'; export const STORAGE_GLOBAL_PREFIX = 'TCAD';
const STORAGE_PREFIX = `${STORAGE_GLOBAL_PREFIX}.projects.`; export const PROJECTS_PREFIX = `${STORAGE_GLOBAL_PREFIX}.projects.`;
export const SKETCH_SUFFIX = '.sketch.';
export function activate(context) { export function activate(context) {
@ -14,15 +15,15 @@ export function activate(context) {
processParams(hints, context); processParams(hints, context);
const sketchNamespace = id + '.sketch.'; const sketchNamespace = id + SKETCH_SUFFIX;
const sketchStorageNamespace = STORAGE_PREFIX + sketchNamespace; const sketchStorageNamespace = PROJECTS_PREFIX + sketchNamespace;
function sketchStorageKey(faceId) { function sketchStorageKey(faceId) {
return sketchStorageNamespace + faceId; return sketchStorageNamespace + faceId;
} }
function projectStorageKey() { function projectStorageKey() {
return STORAGE_PREFIX + id; return PROJECTS_PREFIX + id;
} }
function getSketchURL(sketchId) { function getSketchURL(sketchId) {

View file

@ -1,5 +1,12 @@
import {stream} from '../../../modules/lstream';
export function activate({services}) { export function defineStreams(ctx) {
ctx.streams.storage = {
update: stream()
}
}
export function activate({services, streams}) {
function set(key, value) { function set(key, value) {
localStorage.setItem(key, value); localStorage.setItem(key, value);
@ -13,6 +20,10 @@ export function activate({services}) {
return localStorage.removeItem(key); return localStorage.removeItem(key);
} }
function exists(key) {
return localStorage.hasOwnProperty(key);
}
function getAllKeysFromNamespace(namespace) { function getAllKeysFromNamespace(namespace) {
let keys = []; let keys = [];
for(let i = localStorage.length - 1; i >= 0 ; i--) { for(let i = localStorage.length - 1; i >= 0 ; i--) {
@ -27,8 +38,10 @@ export function activate({services}) {
function addListener(handler) { function addListener(handler) {
window.addEventListener('storage', handler, false); window.addEventListener('storage', handler, false);
} }
addListener(() => streams.storage.update.next(Date.now));
services.storage = { services.storage = {
set, get, remove, addListener, getAllKeysFromNamespace set, get, remove, addListener, getAllKeysFromNamespace, exists
} }
} }

View file

@ -8,6 +8,7 @@
</head> </head>
<body> <body>
<a id="downloader" style="display: none;" ></a> <a id="downloader" style="display: none;" ></a>
<input id="uploader" style="display: none;" type="file">
<div id="app" style="height: 100%;"></div> <div id="app" style="height: 100%;"></div>
<script src="static/index.bundle.js"></script> <script src="static/index.bundle.js"></script>
</body> </body>