mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-09 09:52:34 +01:00
ui toolkit
This commit is contained in:
parent
0128f1b041
commit
e5485dd9be
21 changed files with 845 additions and 41 deletions
|
|
@ -39,6 +39,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"diff-match-patch": "1.0.0",
|
||||
"numeric": "1.2.6"
|
||||
"numeric": "1.2.6",
|
||||
"jwerty": "0.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
web/app/3d/actions/actions.js
Normal file
92
web/app/3d/actions/actions.js
Normal file
|
|
@ -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<br>" + 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;
|
||||
}
|
||||
}
|
||||
|
||||
25
web/app/3d/actions/core-actions.js
Normal file
25
web/app/3d/actions/core-actions.js
Normal file
|
|
@ -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'
|
||||
};
|
||||
70
web/app/3d/actions/operation-actions.js
Normal file
70
web/app/3d/actions/operation-actions.js
Normal file
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
34
web/app/3d/menu/menu-config.js
Normal file
34
web/app/3d/menu/menu-config.js
Normal file
|
|
@ -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' ]
|
||||
};
|
||||
|
||||
91
web/app/3d/menu/menu.js
Normal file
91
web/app/3d/menu/menu.js
Normal file
|
|
@ -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 = $('<div>', {
|
||||
'class' : 'menu'
|
||||
});
|
||||
let container = $('<div>', {'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($('<div>', {'class': 'menu-separator'}));
|
||||
separatorAllowed = false;
|
||||
continue;
|
||||
}
|
||||
separatorAllowed = i != menuActions.length - 1;
|
||||
let menuItem = $('<div>', {'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($('<i>', {'class': 'fa ' + cssIconsToClasses(action.cssIcons)})).append(' ');
|
||||
} else {
|
||||
}
|
||||
menuItem.append($('<span>',{text: action.label}));
|
||||
var hotkey = this.inputManager.keymap[action.id];
|
||||
if (hotkey) {
|
||||
hotkey = hotkey.replace(/\s/g, '');
|
||||
if (hotkey.length < 15) {
|
||||
menuItem.append($('<span>',{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);
|
||||
};
|
||||
|
||||
|
|
@ -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} )
|
||||
|
|
|
|||
48
web/app/3d/operations.js
Normal file
48
web/app/3d/operations.js
Normal file
|
|
@ -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 + ')'
|
||||
};
|
||||
39
web/app/3d/ui/control-bar.js
Normal file
39
web/app/3d/ui/control-bar.js
Normal file
|
|
@ -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 = $('<div>', {'class': 'button'});
|
||||
if (action.cssIcons != undefined) {
|
||||
btn.append($('<i>', {'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;
|
||||
};
|
||||
|
|
@ -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});
|
||||
115
web/app/3d/ui/input-manager.js
Normal file
115
web/app/3d/ui/input-manager.js
Normal file
|
|
@ -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 : '') +
|
||||
('<div>' + action.info + '</div>') +
|
||||
(hotkey ? '<div >hotkey: ' + hotkey + '</div>' : ''));
|
||||
this.showMessageSinkAround();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
11
web/app/3d/ui/keymaps/default.js
Normal file
11
web/app/3d/ui/keymaps/default.js
Normal file
|
|
@ -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'
|
||||
};
|
||||
74
web/app/3d/ui/utils.js
Normal file
74
web/app/3d/ui/utils.js
Normal file
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ export default function ToolBar() {
|
|||
this.node = $('<div>', {
|
||||
css :{
|
||||
'position': 'absolute',
|
||||
'left': '260px',
|
||||
'top': '10px',
|
||||
'background-color': 'rgba(255, 255, 255, 0.5)',
|
||||
'padding': '5px',
|
||||
'border-radius' : '5px'
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,15 @@
|
|||
<div id="right-panel"></div>
|
||||
<div id="viewer-container"></div>
|
||||
<div id="control-bar">
|
||||
<div class="left-group">
|
||||
</div>
|
||||
<div class="right-group">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tab-switcher">
|
||||
</div>
|
||||
<div id="tab-switcher"></div>
|
||||
<div id="message-sink"></div>
|
||||
<a id="downloader" style="display: none;" ></a>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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 ) {
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue