diff --git a/package.json b/package.json
index 2d20188a..691a53e5 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,7 @@
},
"dependencies": {
"diff-match-patch": "1.0.0",
- "numeric": "1.2.6"
+ "numeric": "1.2.6",
+ "jwerty": "0.3.2"
}
}
diff --git a/web/app/3d/actions/actions.js b/web/app/3d/actions/actions.js
new file mode 100644
index 00000000..20fc3c18
--- /dev/null
+++ b/web/app/3d/actions/actions.js
@@ -0,0 +1,92 @@
+export function cssIconsToClasses(cssIcons) {
+ return cssIcons.map((i)=> 'fa-'+i).join(' ')
+}
+
+export function ActionManager(app) {
+ this.app = app;
+ this.actions = {};
+ this.eventsToActions = {};
+ this.registerAction('-', {'type': 'separator'});
+}
+
+ActionManager.prototype.registerAction = function(id, action) {
+ action = Object.assign({id: id}, action);
+ action.__handler = handler(action);
+ action.state = {
+ hint: '',
+ enabled: true,
+ visible: true
+ };
+ this.addListeners(action);
+ this.actions[id] = action;
+};
+
+ActionManager.prototype.addListeners = function(action) {
+ if (action.listens == undefined || action.update == undefined) return;
+ for (let event of action.listens) {
+ let actions = this.eventsToActions[event];
+ if (actions == undefined) {
+ actions = [];
+ this.eventsToActions[event] = actions;
+ this.app.bus.subscribe(event, (data) => this.notify(event));
+ }
+ actions.push(action);
+ }
+ this.updateAction(action);
+};
+
+ActionManager.prototype.notify = function(event) {
+ let actions = this.eventsToActions[event];
+ if (actions != undefined) {
+ for (let action of actions) {
+ this.updateAction(action);
+ this.app.bus.notify('action.update.' + action.id, action.state);
+ }
+ }
+};
+
+ActionManager.prototype.updateAction = function(action) {
+ action.state.hint = '';
+ action.state.enabled = true;
+ action.state.visible = true;
+ action.update(action.state, this.app);
+};
+
+ActionManager.prototype.registerActions = function(actions) {
+ for (let actionName in actions) {
+ this.registerAction(actionName, actions[actionName]);
+ }
+};
+
+ActionManager.prototype.run = function(actionId, event) {
+ var action = this.actions[actionId];
+ if (action == undefined) {
+ return;
+ }
+ if (action.state.enabled) {
+ action.__handler(this.app, event);
+ } else {
+ this.app.inputManager.info("action '"+actionId+"' is disabled and can't be launched
" + action.state.hint);
+ }
+};
+
+ActionManager.prototype.subscribe = function(actionId, callback) {
+ this.app.bus.subscribe('action.update.'+actionId, callback);
+};
+
+const NOOP = () => {};
+
+function handler(action) {
+ if (action.type == 'binary') {
+ return (app, event, source) => app.state[action.property] = !app.state[action.property];
+ } else if (action.type == 'separator') {
+ return NOOP;
+ } else if (action.type == 'menu') {
+ return (app, event) => action.menu.show(app, event);
+ } else if (action.invoke != undefined) {
+ return (app, event) => action.invoke(app, event);
+ } else {
+ return NOOP;
+ }
+}
+
diff --git a/web/app/3d/actions/core-actions.js b/web/app/3d/actions/core-actions.js
new file mode 100644
index 00000000..5d6a005a
--- /dev/null
+++ b/web/app/3d/actions/core-actions.js
@@ -0,0 +1,25 @@
+export const refreshSketches = {
+ cssIcons: ['refresh'],
+ label: 'Refresh Sketches',
+ info: 'refresh all visible sketches',
+ invoke: (app) => app.refreshSketches()
+};
+
+export const info = {
+ cssIcons: ['info-circle'],
+ label: 'info',
+ info: 'opens help dialog',
+ invoke: (app) => app.showInfo()
+};
+
+export const showSketches = {
+ type: 'binary',
+ property: 'showSketches',
+ cssIcons: ['image'],
+ label: 'show sketches',
+ info: 'toggle whether show sketches on a solid face'
+};
+
+export const noIcon = {
+ label: 'no icon'
+};
\ No newline at end of file
diff --git a/web/app/3d/actions/operation-actions.js b/web/app/3d/actions/operation-actions.js
new file mode 100644
index 00000000..1fcd4caf
--- /dev/null
+++ b/web/app/3d/actions/operation-actions.js
@@ -0,0 +1,70 @@
+import * as Operations from '../operations'
+
+function mergeInfo(op, action) {
+ action.label = op.label;
+ action.icon32 = op.icon + '32.png';
+ action.icon96 = op.icon + '96.png';
+ return action;
+}
+
+export const OperationActions = {
+
+ 'CUT': mergeInfo(Operations.CUT, {
+ info: 'makes a cut based on 2D sketch'
+ }),
+
+ 'PAD': mergeInfo(Operations.PAD, {
+ info: 'extrudes 2D sketch'
+ }),
+
+ 'BOX': mergeInfo(Operations.BOX, {
+ info: 'creates new object box'
+ }),
+
+ 'PLANE': mergeInfo(Operations.PLANE, {
+ info: 'creates new object plane'
+ }),
+
+ 'SPHERE': mergeInfo(Operations.SPHERE, {
+ info: 'creates new object sphere'
+ }),
+
+ 'INTERSECTION': mergeInfo(Operations.INTERSECTION, {
+ info: 'intersection operation on two solids'
+ }),
+
+ 'DIFFERENCE': mergeInfo(Operations.DIFFERENCE, {
+ info: 'difference operation on two solids'
+ }),
+
+ 'UNION': mergeInfo(Operations.UNION, {
+ info: 'union operation on two solids'
+ })
+};
+
+requiresFaceSelection(OperationActions.CUT, 1);
+requiresFaceSelection(OperationActions.PAD, 1);
+
+requiresSolidSelection(OperationActions.INTERSECTION, 2);
+requiresSolidSelection(OperationActions.DIFFERENCE, 2);
+requiresSolidSelection(OperationActions.UNION, 2);
+
+function requiresFaceSelection(action, amount) {
+ action.listens = ['selection'];
+ action.update = (state, app) => {
+ state.enabled = app.viewer.selectionMgr.selection.length >= amount;
+ if (!state.enabled) {
+ state.hint = 'requires at least one face to be selected';
+ }
+ }
+}
+
+function requiresSolidSelection(action, amount) {
+ action.listens = ['selection'];
+ action.update = (state, app) => {
+ state.enabled = app.viewer.selectionMgr.selection.length >= amount;
+ if (!state.enabled) {
+ state.hint = 'requires at least two solids to be selected';
+ }
+ }
+}
diff --git a/web/app/3d/menu/menu-config.js b/web/app/3d/menu/menu-config.js
new file mode 100644
index 00000000..4a1d4ead
--- /dev/null
+++ b/web/app/3d/menu/menu-config.js
@@ -0,0 +1,34 @@
+export const file = {
+ label: 'file',
+ actions: ['save', 'exportStl', 'uploadToThingiverse']
+};
+
+export const craft = {
+ label: 'craft',
+ cssIcons: ['magic'],
+ info: 'set of available craft operations on a solid',
+ actions: ['PAD', 'CUT']
+};
+
+export const primitives = {
+ label: 'add',
+ cssIcons: ['cube', 'plus'],
+ info: 'set of available solid creation operations',
+ actions: ['PLANE', 'BOX', 'SPHERE']
+};
+
+export const boolean = {
+ label: 'bool',
+ cssIcons: ['pie-chart'],
+ info: 'set of available boolean operations',
+ actions: ['INTERSECTION', 'DIFFERENCE', 'UNION']
+};
+
+export const main = {
+ label: 'start',
+ cssIcons: ['rocket'],
+ info: 'common set of actions',
+ actions: ['PAD', 'CUT', '-', 'INTERSECTION', 'DIFFERENCE', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-',
+ 'deselectAll', 'refreshSketches' ]
+};
+
diff --git a/web/app/3d/menu/menu.js b/web/app/3d/menu/menu.js
new file mode 100644
index 00000000..c96e7a55
--- /dev/null
+++ b/web/app/3d/menu/menu.js
@@ -0,0 +1,91 @@
+import {cssIconsToClasses} from '../actions/actions'
+import {EventData} from '../ui/utils'
+
+
+export default function Menu(menuActions, inputManager) {
+ this.inputManager = inputManager;
+ this.node = $('
', {
+ 'class' : 'menu'
+ });
+ let container = $('
', {'class': 'menu-container'});
+ this.node.append(container);
+ let separatorAllowed = false;
+ for (var i = 0; i < menuActions.length; i++) {
+ var action = menuActions[i];
+ if (action.type == 'separator') {
+ container.append($('
', {'class': 'menu-separator'}));
+ separatorAllowed = false;
+ continue;
+ }
+ separatorAllowed = i != menuActions.length - 1;
+ let menuItem = $('
', {'class' : 'menu-item action-item'});
+ menuItem.data('action', action.id);
+ menuItem.addClass('icon16-left');
+ if (action.icon32 != undefined) {
+ menuItem.css({
+ 'background-image' : 'url('+action.icon32+')'
+ });
+ } else if (action.cssIcons != undefined) {
+ menuItem.append($('
', {'class': 'fa ' + cssIconsToClasses(action.cssIcons)})).append(' ');
+ } else {
+ }
+ menuItem.append($('',{text: action.label}));
+ var hotkey = this.inputManager.keymap[action.id];
+ if (hotkey) {
+ hotkey = hotkey.replace(/\s/g, '');
+ if (hotkey.length < 15) {
+ menuItem.append($('',{text: hotkey,'class' : 'action-hotkey-info'}));
+ }
+ }
+
+ container.append(menuItem);
+ var uiUpdater = (state) => {
+ if (state.enabled) {
+ menuItem.removeClass('action-disabled');
+ } else {
+ menuItem.addClass('action-disabled');
+ }
+ menuItem.data('actionHint', state.hint);
+ };
+ uiUpdater(action.state);
+ this.inputManager.app.actionManager.subscribe(action.id, uiUpdater);
+ }
+ this.node.hide();
+ $('body').append(this.node);
+};
+
+Menu.prototype.show = function(app, event) {
+ this.node.removeClass('menu-flat-top');
+ this.node.removeClass('menu-flat-bottom');
+ this.node.show(); //node should be visible to get right dimensions
+ const r = Math.round;
+ let source = EventData.get(event, 'menu-button');
+ if (source != undefined) {
+ var off = source.offset();
+ var orientation = source.data('menuOrientation');
+ if (orientation == 'up') {
+ this.node.addClass('menu-flat-bottom');
+ this.node.offset({
+ left: r(off.left),
+ top: r(off.top - this.node.outerHeight())
+ });
+ } else if (orientation == 'down') {
+ this.node.addClass('menu-flat-top');
+ this.node.offset({
+ left: r(off.left),
+ top: r(off.top + source.outerHeight())
+ });
+ } else {
+ }
+ } else {
+ var mouseInfo = this.inputManager.mouseInfo;
+ if (mouseInfo != null) {
+ this.node.offset({
+ left: r(mouseInfo.pageX - this.node.outerWidth() / 2),
+ top: r(mouseInfo.pageY - this.node.outerHeight() / 2)
+ });
+ }
+ }
+ this.inputManager.registerOpenMenu(this);
+};
+
diff --git a/web/app/3d/modeler-app.js b/web/app/3d/modeler-app.js
index f9dee3cb..fdd1d8f8 100644
--- a/web/app/3d/modeler-app.js
+++ b/web/app/3d/modeler-app.js
@@ -1,7 +1,12 @@
import {Bus} from '../ui/toolkit'
import {Viewer} from './viewer'
-import {UI} from './ctrl'
-import TabSwitcher from './tab-switcher'
+import {UI} from './ui/ctrl'
+import TabSwitcher from './ui/tab-switcher'
+import ControlBar from './ui/control-bar'
+import {InputManager} from './ui/input-manager'
+import {ActionManager} from './actions/actions'
+import * as CoreActions from './actions/core-actions'
+import {OperationActions} from './actions/operation-actions'
import Vector from '../math/vector'
import {Matrix3, AXIS, ORIGIN, IDENTITY_BASIS} from '../math/l3space'
import * as workbench from './workbench'
@@ -19,7 +24,15 @@ function App() {
this.initSample();
}
this.bus = new Bus();
+ this.actionManager = new ActionManager(this);
+ this.inputManager = new InputManager(this);
+ this.state = this.createState();
this.viewer = new Viewer(this.bus, document.getElementById('viewer-container'));
+ this.actionManager.registerActions(CoreActions);
+ this.actionManager.registerActions(OperationActions);
+ this.tabSwitcher = new TabSwitcher($('#tab-switcher'), $('#view-3d'));
+ this.controlBar = new ControlBar(this, $('#control-bar'));
+
this.ui = new UI(this);
this.craft = new workbench.Craft(this);
@@ -29,8 +42,6 @@ function App() {
this.load();
}
- this.tabSwitcher = new TabSwitcher($('#tab-switcher'), $('#view-3d'));
-
this._refreshSketches();
this.viewer.render();
@@ -47,6 +58,7 @@ function App() {
app.viewer.render();
}
}
+ window.addEventListener('storage', storage_handler, false);
this.bus.subscribe("craft", function() {
var historyEditMode = app.craft.historyPointer != app.craft.history.length;
@@ -55,9 +67,14 @@ function App() {
}
app._refreshSketches();
});
- window.addEventListener('storage', storage_handler, false);
}
+App.prototype.createState = function() {
+ const state = {};
+ this.bus.defineObservable(state, 'showSketches', true);
+ return state;
+};
+
App.prototype.findAllSolids = function() {
return this.viewer.workGroup.children
.filter(function(obj) {return obj.__tcad_solid !== undefined} )
diff --git a/web/app/3d/operations.js b/web/app/3d/operations.js
new file mode 100644
index 00000000..78432cb8
--- /dev/null
+++ b/web/app/3d/operations.js
@@ -0,0 +1,48 @@
+
+export const CUT = {
+ icon: 'img/3d/cut',
+ label: 'Cut',
+ info: (p) => 'CUT (' + p + ')'
+};
+
+export const PAD = {
+ icon: 'img/3d/extrude',
+ label: 'Extrude',
+ info: (p) => 'PAD (' + p + ')'
+};
+
+export const BOX = {
+ icon: 'img/3d/cube',
+ label: 'Box',
+ info: (p) => 'BOX (' + p + ')'
+};
+
+export const PLANE = {
+ icon: 'img/3d/plane',
+ label: 'Plane',
+ info: (p) => 'PLANE (' + p + ')'
+};
+
+export const SPHERE = {
+ icon: 'img/3d/sphere',
+ label: 'Sphere',
+ info: (p) => 'SPHERE (' + p + ')'
+};
+
+export const INTERSECTION = {
+ icon: 'img/3d/intersection',
+ label: 'Intersection',
+ info: (p) => 'INTERSECTION (' + p + ')'
+};
+
+export const DIFFERENCE = {
+ icon: 'img/3d/difference',
+ label: 'Difference',
+ info: (p) => 'DIFFERENCE (' + p + ')'
+};
+
+export const UNION = {
+ icon: 'img/3d/union',
+ label: 'Union',
+ info: (p) => 'UNION (' + p + ')'
+};
diff --git a/web/app/3d/ui/control-bar.js b/web/app/3d/ui/control-bar.js
new file mode 100644
index 00000000..dad05dd1
--- /dev/null
+++ b/web/app/3d/ui/control-bar.js
@@ -0,0 +1,39 @@
+import {cssIconsToClasses} from '../actions/actions'
+
+export default function ControlBar(app, bar) {
+ this.app = app;
+ this.bar = bar;
+}
+
+ControlBar.prototype.add = function(actionName, left, overrides) {
+ let action = this.app.actionManager.actions[actionName];
+ if (action == undefined) return;
+ if (overrides != undefined) {
+ action = Object.assign({}, action, overrides);
+ }
+ const btn = $('', {'class': 'button'});
+ if (action.cssIcons != undefined) {
+ btn.append($('
', {'class': 'fa ' + cssIconsToClasses(action.cssIcons)}));
+ }
+ if (action.label != undefined && action.label != null) {
+ if (action.cssIcons != undefined) {
+ btn.append(' ');
+ }
+ btn.append(action.label);
+ }
+ var to = this.bar.find(left ? '.left-group' : '.right-group');
+ to.append(btn);
+ if (action.type == 'binary') {
+ this.app.bus.subscribe(action.property, (show) => {
+ btn.removeClass('button-selected');
+ if (show) {
+ btn.addClass('button-selected');
+ }
+ })(this.app.state[action.property]);
+ } else if (action.type == 'menu') {
+ btn.data('menuOrientation', 'up');
+ }
+ btn.addClass('action-item');
+ btn.data('action', actionName);
+ return btn;
+};
\ No newline at end of file
diff --git a/web/app/3d/ctrl.js b/web/app/3d/ui/ctrl.js
similarity index 81%
rename from web/app/3d/ctrl.js
rename to web/app/3d/ui/ctrl.js
index 12122db0..ba636fae 100644
--- a/web/app/3d/ctrl.js
+++ b/web/app/3d/ui/ctrl.js
@@ -1,14 +1,17 @@
-import * as tk from '../ui/toolkit'
-import * as cad_utils from './cad-utils'
-import * as math from '../math/math'
-import * as workbench from './workbench'
-import ToolBar from '../ui/toolbar'
-import {ExtrudeWizard} from './wizards/extrude'
-import {PlaneWizard} from './wizards/plane'
-import {BoxWizard} from './wizards/box'
-import {SphereWizard} from './wizards/sphere'
-import {TransformWizard} from './wizards/transform'
-import {IO} from '../sketcher/io'
+import * as tk from '../../ui/toolkit'
+import * as cad_utils from '../cad-utils'
+import * as math from '../../math/math'
+import * as workbench from '../workbench'
+import ToolBar from '../../ui/toolbar'
+import * as MenuConfig from '../menu/menu-config'
+import * as Operations from '../operations'
+import Menu from '../menu/menu'
+import {ExtrudeWizard} from '../wizards/extrude'
+import {PlaneWizard} from '../wizards/plane'
+import {BoxWizard} from '../wizards/box'
+import {SphereWizard} from '../wizards/sphere'
+import {TransformWizard} from '../wizards/transform'
+import {IO} from '../../sketcher/io'
function UI(app) {
this.app = app;
@@ -37,10 +40,13 @@ function UI(app) {
tk.add(modificationsFolder, modificationsListComp);
var toolbarVertOffset = 10; //this.mainBox.root.position().top;
+
+ this.registerMenuActions();
+
this.craftToolBar = this.createCraftToolBar(toolbarVertOffset);
this.createBoolToolBar(this.craftToolBar.node.position().top + this.craftToolBar.node.height() + 20);
this.createMiscToolBar(toolbarVertOffset);
-
+ this.fillControlBar();
var ui = this;
function setHistory() {
@@ -119,8 +125,7 @@ function UI(app) {
printFaceId.root.click(function () {
console.log(app.viewer.selectionMgr.selection[0].id);
});
- showSketches.input.click(function () {
- var enabled = this.checked;
+ this.app.bus.subscribe("showSketches", (enabled) => {
var solids = app.findAllSolids();
for (var i = 0; i < solids.length; i++) {
for (var j = 0; j < solids[i].polyFaces.length; j++) {
@@ -130,6 +135,7 @@ function UI(app) {
}
app.viewer.render();
});
+
save.root.click(function() {
app.save();
});
@@ -166,8 +172,8 @@ UI.prototype.createCraftToolBar = function (vertPos) {
toolBar.add('Plane', 'img/3d/plane96.png', () => this.registerWizard(new PlaneWizard(this.app.viewer), false));
toolBar.add('Box', 'img/3d/cube96.png', () => this.registerWizard(new BoxWizard(this.app.viewer), false));
toolBar.add('Sphere', 'img/3d/sphere96.png', () => this.registerWizard(new SphereWizard(this.app.viewer), false));
- $('#view-3d').append(toolBar.node);
- toolBar.node.css({top : vertPos + 'px'});
+ $('#viewer-container').append(toolBar.node);
+ toolBar.node.css({left: '10px',top : vertPos + 'px'});
return toolBar;
};
@@ -177,9 +183,9 @@ UI.prototype.createMiscToolBar = function (vertPos) {
toolBar.addFa('upload', () => this.app.sketchFace());
toolBar.addFa('refresh', () => this.app.sketchFace());
toolBar.addFa('square-o', () => this.app.sketchFace());
- $('#view-3d').append(toolBar.node);
+ $('#viewer-container').append(toolBar.node);
toolBar.node.css({top : vertPos + 'px'});
- toolBar.node.css({left : '', right: '20px', 'font-size': '16px'});
+ toolBar.node.css({right: '10px', 'font-size': '16px'});
return toolBar;
};
@@ -188,11 +194,33 @@ UI.prototype.createBoolToolBar = function(vertPos) {
toolBar.add('Intersection', 'img/3d/intersection96.png', () => this.app.sketchFace());
toolBar.add('Difference', 'img/3d/difference96.png', this.cutExtrude(true));
toolBar.add('Union', 'img/3d/union96.png', this.cutExtrude(false));
- $('#view-3d').append(toolBar.node);
- toolBar.node.css({top : vertPos + 'px'});
+ $('#viewer-container').append(toolBar.node);
+ toolBar.node.css({left: '10px', top : vertPos + 'px'});
return toolBar;
};
+UI.prototype.registerMenuActions = function() {
+ for (let menuName in MenuConfig) {
+ const m = MenuConfig[menuName];
+ var action = Object.assign({'type' : 'menu'}, m);
+ delete action['actions'];
+ action.menu = new Menu(
+ m.actions.map((a) => this.app.actionManager.actions[a])
+ .filter((a) => a != undefined), this.app.inputManager);
+ this.app.actionManager.registerAction('menu.' + menuName, action);
+ }
+};
+
+UI.prototype.fillControlBar = function() {
+ const LEFT = true;
+ const RIGHT = !LEFT;
+ this.app.controlBar.add('info', RIGHT, {'label': null});
+ this.app.controlBar.add('refreshSketches', RIGHT, {'label': null});
+ this.app.controlBar.add('showSketches', RIGHT, {'label': 'sketches'});
+ this.app.controlBar.add('menu.craft', LEFT);
+ this.app.controlBar.add('menu.primitives', LEFT);
+ this.app.controlBar.add('menu.boolean', LEFT);
+};
UI.prototype.registerWizard = function(wizard, overridingHistory) {
wizard.ui.box.root.css({left : (this.mainBox.root.width() + this.craftToolBar.node.width() + 30) + 'px', top : 0});
diff --git a/web/app/3d/ui/input-manager.js b/web/app/3d/ui/input-manager.js
new file mode 100644
index 00000000..0040e981
--- /dev/null
+++ b/web/app/3d/ui/input-manager.js
@@ -0,0 +1,115 @@
+import {jwerty} from 'jwerty'
+import {keymap} from './keymaps/default'
+import {DefaultMouseEvent, EventData, fit} from './utils'
+
+export function InputManager(app) {
+ this.app = app;
+ this.openMenus = [];
+ this.keymap = keymap;
+ this.mouseInfo = new DefaultMouseEvent();
+ this.requestedActionInfo = null;
+ $(() => {
+ $(document)
+ .on('keydown', (e) => this.handleKeyPress(e))
+ .on('mousedown', (e) => this.clear(e))
+ .on('mouseenter', '.action-item', (e) => this.showActionInfo($(e.target)))
+ .on('mouseleave', '.action-item', (e) => this.emptyInfo())
+ .on('mousemove', (e) => this.mouseInfo = e)
+ .on('click', '.action-item', (e) => this.handleActionClick(e));
+ });
+}
+
+InputManager.prototype.handleKeyPress = function(e) {
+ console.log(e.keyCode);
+ switch (e.keyCode) {
+ case 27 : this.clear(); break;
+ }
+
+ for (let action in this.keymap) {
+ if (jwerty.is(this.keymap[action], e)) {
+ this.app.actionManager.run(action, e);
+ break;
+ }
+ }
+};
+
+InputManager.prototype.clear = function(e) {
+ if (e != undefined && $(e.target).closest('.menu-item').length != 0) {
+ return;
+ }
+ if (this.openMenus.length != 0) {
+ for (let openMenu of this.openMenus) {
+ openMenu.node.hide();
+ }
+ this.openMenus = [];
+ }
+ this.requestedActionInfo = null;
+ $('#message-sink').hide();
+};
+
+InputManager.prototype.handleActionClick = function(event) {
+ var target = $(event.currentTarget);
+ var action = target.data('action');
+ if (action != undefined) {
+ this.clear();
+ EventData.set(event, 'menu-button', target);
+ this.app.actionManager.run(action, event);
+ }
+};
+
+InputManager.prototype.registerOpenMenu = function(menu) {
+ fit(menu.node, $('body'));
+ this.openMenus.push(menu);
+};
+
+InputManager.messageSink = function() {
+ return $('#message-sink');
+};
+
+InputManager.prototype.emptyInfo = function() {
+ this.requestedActionInfo = null;
+ var messageSink = InputManager.messageSink();
+ messageSink.empty();
+ messageSink.hide();
+};
+
+InputManager.prototype.showActionInfo = function(el) {
+ //show hint immediately and deffer showing the full info
+ var hint = el.data('actionHint');
+ if (hint) {
+ InputManager.messageSink().text(hint);
+ this.showMessageSinkAround();
+ }
+ this.requestInfo(el.data('action'));
+};
+
+InputManager.prototype.info = function(text) {
+ InputManager.messageSink().html(text);
+ this.showMessageSinkAround();
+};
+
+InputManager.prototype.showMessageSinkAround = function() {
+ var messageSink = InputManager.messageSink();
+ messageSink.show();
+ messageSink.offset({left: this.mouseInfo.pageX + 10, top: this.mouseInfo.pageY + 10});
+ fit(messageSink, $('body'));
+};
+
+InputManager.prototype.requestInfo = function(action) {
+ this.requestedActionInfo = action;
+ setTimeout(() => {
+ var actionId = this.requestedActionInfo;
+ this.requestedActionInfo = null;
+ if (actionId != null) {
+ const action = this.app.actionManager.actions[actionId];
+ if (action) {
+ var hotkey = this.keymap[actionId];
+ InputManager.messageSink().html(
+ (action.state.hint ? action.state.hint : '') +
+ ('' + action.info + '
') +
+ (hotkey ? 'hotkey: ' + hotkey + '
' : ''));
+ this.showMessageSinkAround();
+ }
+ }
+ }, 1000);
+};
\ No newline at end of file
diff --git a/web/app/3d/ui/keymaps/default.js b/web/app/3d/ui/keymaps/default.js
new file mode 100644
index 00000000..e7d682cc
--- /dev/null
+++ b/web/app/3d/ui/keymaps/default.js
@@ -0,0 +1,11 @@
+export const keymap = {
+
+ 'CUT': 'C',
+ 'PAD': 'E',
+ 'zoomIn': '+',
+ 'zoomOut': '-',
+ 'menu.craft': 'shift+C',
+ 'menu.primitives': 'shift+A',
+ 'menu.main': 'space',
+ 'save': 'CTRL + S'
+};
diff --git a/web/app/3d/tab-switcher.js b/web/app/3d/ui/tab-switcher.js
similarity index 100%
rename from web/app/3d/tab-switcher.js
rename to web/app/3d/ui/tab-switcher.js
diff --git a/web/app/3d/ui/utils.js b/web/app/3d/ui/utils.js
new file mode 100644
index 00000000..faa6a80e
--- /dev/null
+++ b/web/app/3d/ui/utils.js
@@ -0,0 +1,74 @@
+export function DefaultMouseEvent() {
+ var viewer = $('#viewer-container');
+ var off = viewer.offset();
+ const r = Math.round;
+ this.type = 'click';
+ this.canBubble = true;
+ this.cancelable = true;
+ this.detail = 1;
+ this.screenX = r(off.left + viewer.width() / 2);
+ this.screenY = r(off.top + viewer.height() / 2);
+ this.clientX = this.screenX;
+ this.clientY = this.screenY;
+ this.pageX = this.screenX;
+ this.pageY = this.screenY;
+ this.ctrlKey = false;
+ this.altKey = false;
+ this.shiftKey = false;
+ this.metaKey = false;
+ this.button = 0;
+ this.relatedTarget = null;
+}
+
+export const EventData = {
+
+ get: function(event, key) {
+ if (event.data) {
+ return event.data[key];
+ } else {
+ return undefined;
+ }
+ },
+
+ set: function(event, key, value) {
+ if (!event.data) {
+ event.data = {};
+ }
+ event.data[key] = value;
+ }
+};
+
+export function fit(el, relativeEl) {
+ const span = 5;
+ var relOff = relativeEl.offset();
+ var off = el.offset();
+
+ var needToSet = false;
+ if (off.left < relOff.left ) {
+ off.left = relOff.left + span;
+ needToSet = true;
+ }
+ const right = relOff.left + relativeEl.width() - span;
+ var outerWidth = el.outerWidth();
+ if (off.left + outerWidth >= right) {
+ off.left = right - outerWidth;
+ needToSet = true;
+ }
+ if (off.top < relOff.top + span) {
+ off.top = relOff.top + span;
+ needToSet = true;
+ }
+ var bottom = relOff.top + relativeEl.height() - span;
+ var outerHeight = el.outerHeight();
+ if (off.top + outerHeight >= bottom) {
+ off.top = bottom - outerHeight;
+ needToSet = true;
+ }
+ if (needToSet) {
+ el.css({
+ left: off.left + 'px',
+ top: off.top + 'px'
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/web/app/3d/viewer.js b/web/app/3d/viewer.js
index 2e115c27..781d5bd4 100644
--- a/web/app/3d/viewer.js
+++ b/web/app/3d/viewer.js
@@ -121,14 +121,24 @@ function Viewer(bus, container) {
startY : 0
};
+ //fix for FireFox
+ function fixOffsetAPI(event) {
+ if (event.offsetX == undefined) {
+ event.offsetX = event.layerX;
+ event.offsetY = event.layerY;
+ }
+ }
+
renderer.domElement.addEventListener('mousedown',
function(e) {
+ fixOffsetAPI(e);
mouseState.startX = e.offsetX;
mouseState.startY = e.offsetY;
}, false);
renderer.domElement.addEventListener('mouseup',
function(e) {
+ fixOffsetAPI(e);
var dx = Math.abs(mouseState.startX - e.offsetX);
var dy = Math.abs(mouseState.startY - e.offsetY);
var TOL = 1;
@@ -258,6 +268,7 @@ SelectionManager.prototype.deselectAll = function() {
this.selection[i].solid.mesh.geometry.colorsNeedUpdate = true;
}
this.clear();
+ this.viewer.bus.notify('selection', null);
this.viewer.render();
};
diff --git a/web/app/sketcher/viewer2d.js b/web/app/sketcher/viewer2d.js
index 8de63a3f..4aada723 100644
--- a/web/app/sketcher/viewer2d.js
+++ b/web/app/sketcher/viewer2d.js
@@ -101,7 +101,7 @@ function Viewer(canvas, IO) {
this._serviceLayers = [];
this.dimLayer = new Layer("_dim", Styles.DIM);
this.dimLayers = [this.dimLayer];
- this.bus.defineObservable(this, 'dimScale', 'dimScale', 1);
+ this.bus.defineObservable(this, 'dimScale', 1);
this.bus.subscribe('dimScale', function(){ viewer.refresh(); });
this._workspace = [this.dimLayers, this.layers, this._serviceLayers];
diff --git a/web/app/ui/toolbar.js b/web/app/ui/toolbar.js
index 6d4f9e3e..ae45eaf1 100644
--- a/web/app/ui/toolbar.js
+++ b/web/app/ui/toolbar.js
@@ -3,8 +3,6 @@ export default function ToolBar() {
this.node = $('', {
css :{
'position': 'absolute',
- 'left': '260px',
- 'top': '10px',
'background-color': 'rgba(255, 255, 255, 0.5)',
'padding': '5px',
'border-radius' : '5px'
diff --git a/web/app/ui/toolkit.js b/web/app/ui/toolkit.js
index 21369a07..94a6ee11 100644
--- a/web/app/ui/toolkit.js
+++ b/web/app/ui/toolkit.js
@@ -278,20 +278,26 @@ export function Bus() {
this.listeners = {};
}
-Bus.prototype.subscribe = function(event, callback) {
+Bus.prototype.subscribe = function(event, callback, listenerId) {
var listenerList = this.listeners[event];
if (listenerList === undefined) {
listenerList = [];
this.listeners[event] = listenerList;
}
- listenerList.push(callback);
+ if (listenerId == undefined) listenerId = null;
+ listenerList.push([callback, listenerId]);
+ return callback;
};
-Bus.prototype.notify = function(event, data) {
+Bus.prototype.notify = function(event, data, sender) {
var listenerList = this.listeners[event];
if (listenerList !== undefined) {
for (var i = 0; i < listenerList.length; i++) {
- listenerList[i](data);
+ const callback = listenerList[i][0];
+ const listenerId = listenerList[i][1];
+ if (sender == undefined || listenerId == null || listenerId != sender) {
+ callback(data);
+ }
}
}
};
@@ -300,7 +306,8 @@ Bus.Observable = function(initValue) {
this.value = initValue;
};
-Bus.prototype.defineObservable = function(scope, name, eventName, initValue) {
+Bus.prototype.defineObservable = function(scope, name, initValue, eventName) {
+ if (eventName == undefined) eventName = name;
var observable = new Bus.Observable(initValue);
var bus = this;
return Object.defineProperty(scope, name, {
diff --git a/web/css/app3d.less b/web/css/app3d.less
index 1e89aa12..c7c1b75c 100644
--- a/web/css/app3d.less
+++ b/web/css/app3d.less
@@ -6,6 +6,9 @@
@right-panel-width: 250px;
+@control-button-border: 1px solid #2c2c2c;
+@menu-border-radius: 3px;
+
.no-selection {
user-select: none;
-webkit-user-select: none;
@@ -15,6 +18,11 @@
body {
background-color: #808080;
+ .main-font;
+}
+
+.main-font {
+ font: 11px 'Lucida Grande', sans-serif;
}
.history-selected, .history-selected:hover {
@@ -36,7 +44,6 @@ body {
width: 100%;
border-top: 1px solid @tab-border-color;
color: #eee;
- font: 11px 'Lucida Grande', sans-serif;
text-align: center;
.no-selection;
}
@@ -68,15 +75,147 @@ body {
}
#viewer-container {
- position: absolute; left: @right-panel-width; right:0; height: 100%;
+ position: absolute;
+ left: @right-panel-width;
+ right: 0;
+ top: 0;
+ bottom: 0;
}
#control-bar {
-
+ position: absolute;
+ left: @right-panel-width;
+ right: 0;
+ bottom: 0;
+ height: 20px;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: #ccc;
+}
+
+#control-bar .left-group {
+ text-align: left;
+ float: left;
+}
+
+#control-bar .right-group {
+ text-align: right;
+}
+
+#control-bar .left-group .button {
+ float: left;
+ border-right: @control-button-border;
+}
+
+#control-bar .right-group .button {
+ float: right;
+ border-left: @control-button-border;
+}
+
+.button .fa {
+ line-height: 1.5;
+}
+
+#control-bar .button {
+ padding: 3px 7px 0 5px;
+ height: 100%;
+ vertical-align: baseline;
+ cursor: pointer;
+ .no-selection
+}
+
+#control-bar .button:hover {
+ background-color: #555;
+}
+
+#control-bar .button-selected {
+ background-color: #666;
+}
+
+#control-bar .button-selected:hover {
+ background-color: #666;
}
#right-panel {
- position: absolute; height: 100%; width: @right-panel-width;
+ position: absolute;
+ height: 100%;
+ background-color: #000;
+ width: @right-panel-width;
+}
+
+.aux-win {
+ color: #fff;
+ background-color: rgba(0,0,0,0.7);
+ border: solid 1px #000;
+ border-radius: @menu-border-radius
+}
+
+.menu {
+ position: absolute;
+ .aux-win;
+ /* this element can't have neither padding nor margin to be properly positioned as menu */
+}
+
+.menu-container {
+ padding: 5px 0 5px 0;
+}
+
+.menu-item {
+ padding: 5px 5px 5px 2px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.menu-item:hover {
+ background-color: #0074D9;
+}
+
+.menu-flat-bottom {
+ border-radius: @menu-border-radius @menu-border-radius 0 0;
+}
+
+.menu-flat-top {
+ border-radius: 0 0 @menu-border-radius @menu-border-radius;
+}
+
+.menu-separator {
+ border-top: solid 1px #777;
+}
+
+.menu-item .fa {
+ margin-left: -16px;
+ padding-right: 3px;
+}
+
+.menu-item.action-disabled {
+ color: #888;
+}
+
+.menu-item .action-hotkey-info {
+ float: right;
+ padding-left: 15px;
+ color: #888;
+ font-size: 9px;
+ margin-top: 1px;
+}
+
+.icon16-left {
+ background-position-y: center;
+ background-position-x: 5px;
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+ padding-left: 25px;
+}
+
+#message-sink {
+ display: none;
+ position: absolute;
+ max-width: 400px;
+ padding: 5px;
+ .aux-win;
+ color: #ccc;
+ white-space: nowrap;
+ z-index: 999;
}
+
diff --git a/web/index.html b/web/index.html
index 68995f6d..15e9084b 100644
--- a/web/index.html
+++ b/web/index.html
@@ -29,11 +29,15 @@
-
-
+
+