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 @@
+
+
+
+
-
-
+
+
diff --git a/web/lib/three/TrackballControls.js b/web/lib/three/TrackballControls.js index ba95438c..df170dfb 100644 --- a/web/lib/three/TrackballControls.js +++ b/web/lib/three/TrackballControls.js @@ -395,7 +395,7 @@ THREE.TrackballControls = function ( object, domElement ) { if ( _this.enabled === false ) return; event.preventDefault(); - event.stopPropagation(); + //event.stopPropagation(); - interfere with entire application if ( _state === STATE.NONE ) {