mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-06 16:33:15 +01:00
export and import project as a bundle
This commit is contained in:
parent
a1e2b80948
commit
c47d53cd86
11 changed files with 276 additions and 26 deletions
|
|
@ -57,6 +57,25 @@ 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',
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
43
web/app/cad/projectManager/ProjectManager.jsx
Normal file
43
web/app/cad/projectManager/ProjectManager.jsx
Normal 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>
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
9
web/app/cad/projectManager/ProjectManager.less
Normal file
9
web/app/cad/projectManager/ProjectManager.less
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.root a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 90%;
|
||||||
|
margin: 2px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
148
web/app/cad/projectManager/projectManagerPlugin.js
Normal file
148
web/app/cad/projectManager/projectManagerPlugin.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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--) {
|
||||||
|
|
@ -28,7 +39,9 @@ export function activate({services}) {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue