diff --git a/web/app/3d/actions/action-helpers.js b/web/app/3d/actions/action-helpers.js new file mode 100644 index 00000000..515ec898 --- /dev/null +++ b/web/app/3d/actions/action-helpers.js @@ -0,0 +1,17 @@ +export function checkForSelectedFaces(amount) { + return (state, app) => { + state.enabled = app.viewer.selectionMgr.selection.length >= amount; + if (!state.enabled) { + state.hint = amount == 1 ? 'requires a face to be selected' : 'requires ' + amount + ' faces to be selected'; + } + } +} + +export function checkForSelectedSolids(amount) { + return (state, app) => { + state.enabled = app.viewer.selectionMgr.selection.length >= amount; + if (!state.enabled) { + state.hint = amount == 1 ? 'requires a solid to be selected' : 'requires ' + amount + ' solids to be selected'; + } + } +} \ No newline at end of file diff --git a/web/app/3d/actions/actions.js b/web/app/3d/actions/actions.js index 64dd5531..a6a45739 100644 --- a/web/app/3d/actions/actions.js +++ b/web/app/3d/actions/actions.js @@ -72,6 +72,11 @@ ActionManager.prototype.run = function(actionId, event) { ActionManager.prototype.subscribe = function(actionId, callback) { this.app.bus.subscribe('action.update.'+actionId, callback); + const action = this.actions[actionId]; + if (action) { + callback(action.state); + } + return callback; }; const NOOP = () => {}; diff --git a/web/app/3d/actions/core-actions.js b/web/app/3d/actions/core-actions.js index 5d6a005a..26117eb0 100644 --- a/web/app/3d/actions/core-actions.js +++ b/web/app/3d/actions/core-actions.js @@ -1,18 +1,52 @@ -export const refreshSketches = { +import * as ActionHelpers from './action-helpers' + +export const EditFace = { + cssIcons: ['file-picture-o'], + label: 'edit', + icon32: 'img/3d/face-edit32.png', + icon96: 'img/3d/face-edit96.png', + info: 'open sketcher for a face/plane', + listens: ['selection'], + update: ActionHelpers.checkForSelectedFaces(1), + invoke: (app) => app.sketchFace() +}; + +export const Save = { + cssIcons: ['floppy-o'], + label: 'refresh sketches', + info: 'force refreshing sketches/loading from storage', + invoke: (app) => app.save() +}; + +export const StlExport = { + cssIcons: ['upload', 'flip-vertical'], + label: '', + info: 'refresh all visible sketches', + invoke: (app) => app.stlExport() +}; + +export const RefreshSketches = { cssIcons: ['refresh'], label: 'Refresh Sketches', info: 'refresh all visible sketches', invoke: (app) => app.refreshSketches() }; -export const info = { +export const DeselectAll = { + cssIcons: ['square-o'], + label: 'deselect all', + info: 'deselect everything', + invoke: (app) => app.viewer.selectionMgr.deselectAll() +}; + +export const Info = { cssIcons: ['info-circle'], label: 'info', info: 'opens help dialog', invoke: (app) => app.showInfo() }; -export const showSketches = { +export const ShowSketches = { type: 'binary', property: 'showSketches', cssIcons: ['image'], diff --git a/web/app/3d/actions/operation-actions.js b/web/app/3d/actions/operation-actions.js index 1fcd4caf..f093f1ea 100644 --- a/web/app/3d/actions/operation-actions.js +++ b/web/app/3d/actions/operation-actions.js @@ -1,43 +1,46 @@ import * as Operations from '../operations' +import * as ActionHelpers from './action-helpers' -function mergeInfo(op, action) { +function mergeInfo(opName, action) { + const op = Operations[opName]; action.label = op.label; action.icon32 = op.icon + '32.png'; action.icon96 = op.icon + '96.png'; + action.invoke = (app) => app.ui.initOperation(opName); return action; } export const OperationActions = { - 'CUT': mergeInfo(Operations.CUT, { + 'CUT': mergeInfo('CUT', { info: 'makes a cut based on 2D sketch' }), - 'PAD': mergeInfo(Operations.PAD, { + 'PAD': mergeInfo('PAD', { info: 'extrudes 2D sketch' }), - 'BOX': mergeInfo(Operations.BOX, { + 'BOX': mergeInfo('BOX', { info: 'creates new object box' }), - 'PLANE': mergeInfo(Operations.PLANE, { + 'PLANE': mergeInfo('PLANE', { info: 'creates new object plane' }), - 'SPHERE': mergeInfo(Operations.SPHERE, { + 'SPHERE': mergeInfo('SPHERE', { info: 'creates new object sphere' }), - 'INTERSECTION': mergeInfo(Operations.INTERSECTION, { + 'INTERSECTION': mergeInfo('INTERSECTION', { info: 'intersection operation on two solids' }), - 'DIFFERENCE': mergeInfo(Operations.DIFFERENCE, { + 'DIFFERENCE': mergeInfo('DIFFERENCE', { info: 'difference operation on two solids' }), - 'UNION': mergeInfo(Operations.UNION, { + 'UNION': mergeInfo('UNION', { info: 'union operation on two solids' }) }; @@ -51,20 +54,10 @@ 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'; - } - } + action.update = ActionHelpers.checkForSelectedFaces(amount) } 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'; - } - } + action.update = ActionHelpers.checkForSelectedSolids(amount) } diff --git a/web/app/3d/menu/menu-config.js b/web/app/3d/menu/menu-config.js index 4a1d4ead..15fae614 100644 --- a/web/app/3d/menu/menu-config.js +++ b/web/app/3d/menu/menu-config.js @@ -28,7 +28,7 @@ export const main = { label: 'start', cssIcons: ['rocket'], info: 'common set of actions', - actions: ['PAD', 'CUT', '-', 'INTERSECTION', 'DIFFERENCE', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-', - 'deselectAll', 'refreshSketches' ] + actions: ['PAD', 'CUT', '-', 'INTERSECTION', 'DIFFERENCE', 'UNION', '-', 'PLANE', 'BOX', 'SPHERE', '-', + 'EditFace', '-', 'DeselectAll', 'RefreshSketches' ] }; diff --git a/web/app/3d/menu/menu.js b/web/app/3d/menu/menu.js index c96e7a55..ac0c06bd 100644 --- a/web/app/3d/menu/menu.js +++ b/web/app/3d/menu/menu.js @@ -39,16 +39,13 @@ export default function Menu(menuActions, inputManager) { } container.append(menuItem); - var uiUpdater = (state) => { + this.inputManager.app.actionManager.subscribe(action.id, (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); diff --git a/web/app/3d/modeler-app.js b/web/app/3d/modeler-app.js index fdd1d8f8..c6ed94eb 100644 --- a/web/app/3d/modeler-app.js +++ b/web/app/3d/modeler-app.js @@ -12,6 +12,7 @@ import {Matrix3, AXIS, ORIGIN, IDENTITY_BASIS} from '../math/l3space' import * as workbench from './workbench' import * as cad_utils from './cad-utils' import * as math from '../math/math' +import {IO} from '../sketcher/io' require('../../css/app3d.less'); function App() { @@ -388,6 +389,18 @@ App.prototype.load = function() { } }; +App.prototype.stlExport = function() { + var allPolygons = cad_utils.arrFlatten1L(this.findAllSolids().map(function (s) { + return s.csg.toPolygons() + })); + var stl = CSG.fromPolygons(allPolygons).toStlString(); + IO.exportTextData(stl.data[0], app.id + ".stl"); +}; + +App.prototype.showInfo = function() { + alert('men at work'); +}; + App.prototype.initSample = function() { localStorage.setItem("TCAD.projects.sample", '{"history":[{"type":"PLANE","solids":[],"params":{"basis":[[1,0,0],[0,0,1],[0,1,0]],"depth":"0"},"protoParams":["XZ","0"]},{"type":"PAD","solids":[0],"face":"0:0","params":{"target":[0,-50,0],"expansionFactor":"1"},"protoParams":["50","1","0","0"]},{"type":"PAD","solids":[1],"face":"1:1","params":{"target":[0,-50,0],"expansionFactor":"1"},"protoParams":["50","1","0","0"]},{"type":"CUT","solids":[2],"face":"2:0","params":{"target":[0,252,0],"expansionFactor":"1"},"protoParams":["252","1","0","0"]},{"type":"CUT","solids":[3],"face":"1:1$","params":{"target":[0,50,0],"expansionFactor":"1"},"protoParams":["50","1","0","0"]}]}'); localStorage.setItem("TCAD.projects.sample.sketch.0:0", '{"layers":[{"name":"_dim","style":{"lineWidth":1,"strokeStyle":"#bcffc1","fillStyle":"#00FF00"},"data":[]},{"name":"__bounds__","style":{"lineWidth":2,"strokeStyle":"#fff5c3","fillStyle":"#000000"},"data":[{"id":6,"_class":"TCAD.TWO.Segment","aux":true,"edge":0,"points":[[0,[1,-400],[2,400]],[3,[4,-400],[5,-400]]]},{"id":13,"_class":"TCAD.TWO.Segment","aux":true,"edge":2,"points":[[7,[8,-400],[9,-400]],[10,[11,400],[12,-400]]]},{"id":20,"_class":"TCAD.TWO.Segment","aux":true,"edge":4,"points":[[14,[15,400],[16,-400]],[17,[18,400],[19,400]]]},{"id":27,"_class":"TCAD.TWO.Segment","aux":true,"edge":6,"points":[[21,[22,400],[23,400]],[24,[25,-400],[26,400]]]}]},{"name":"sketch","style":{"lineWidth":2,"strokeStyle":"#ffffff","fillStyle":"#000000"},"data":[{"id":34,"_class":"TCAD.TWO.Segment","points":[[28,[29,-80.41502600578134],[30,240.48794311524324]],[31,[32,252.10163324769275],[33,71.15131239804411]]]},{"id":41,"_class":"TCAD.TWO.Segment","points":[[35,[36,255.946878629896],[37,-145.76094357167156]],[38,[39,-91.17342089039929],[40,-338.36716169336114]]]},{"id":48,"_class":"TCAD.TWO.Segment","points":[[42,[43,-172.00749593627577],[44,-240.71428346724593]],[45,[46,-88.51020843368133],[47,-140.46311545122035]]]},{"id":55,"_class":"TCAD.TWO.Segment","points":[[49,[50,-102.18982576106004],[51,18.31440664196805]],[52,[53,-182.7982464866314],[54,86.82364838151852]]]},{"id":72,"_class":"TCAD.TWO.Arc","points":[[63,[64,255.946878629896],[65,-145.76094357167156]],[66,[67,252.10163324769275],[68,71.15131239804411]],[69,[70,196.33682709088268],[71,-38.32745196977044]]]},{"id":83,"_class":"TCAD.TWO.Arc","points":[[74,[75,-80.41502600578134],[76,240.48794311524324]],[77,[78,-182.7982464866314],[79,86.82364838151852]],[80,[81,-122.59914075444685],[82,157.65429488839598]]]},{"id":94,"_class":"TCAD.TWO.Arc","points":[[85,[86,-88.51020843368133],[87,-140.46311545122035]],[88,[89,-102.18982576106004],[90,18.31440664196805]],[91,[92,-175.53398017227],[93,-67.98267439986091]]]},{"id":105,"_class":"TCAD.TWO.Arc","points":[[96,[97,-172.00749593627577],[98,-240.71428346724593]],[99,[100,-91.17342089039929],[101,-338.36716169336114]],[102,[103,-122.4591797419898],[104,-281.9821285194346]]]}]},{"name":"_construction_","style":{"lineWidth":1,"strokeStyle":"#aaaaaa","fillStyle":"#000000"},"data":[]}],"constraints":[["Tangent",[72,41]],["Tangent",[72,34]],["coi",[63,35]],["coi",[66,31]],["Tangent",[83,34]],["Tangent",[83,55]],["coi",[74,28]],["coi",[77,52]],["Tangent",[94,48]],["Tangent",[94,55]],["coi",[85,45]],["coi",[88,49]],["Tangent",[105,48]],["Tangent",[105,41]],["coi",[96,42]],["coi",[99,38]]],"boundary":{"lines":[{"a":{"x":-400,"y":400},"b":{"x":-400,"y":-400}},{"a":{"x":-400,"y":-400},"b":{"x":400,"y":-400}},{"a":{"x":400,"y":-400},"b":{"x":400,"y":400}},{"a":{"x":400,"y":400},"b":{"x":-400,"y":400}}],"arcs":[],"circles":[]}}'); diff --git a/web/app/3d/ui/ctrl.js b/web/app/3d/ui/ctrl.js index ba636fae..5529cce9 100644 --- a/web/app/3d/ui/ctrl.js +++ b/web/app/3d/ui/ctrl.js @@ -11,7 +11,6 @@ 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; @@ -142,13 +141,6 @@ function UI(app) { deselectAll.root.click(function() { app.viewer.selectionMgr.deselectAll(); }); - stlExport.root.click(function() { - var allPolygons = cad_utils.arrFlatten1L(app.findAllSolids().map(function (s) { - return s.csg.toPolygons() - })); - var stl = CSG.fromPolygons(allPolygons).toStlString(); - IO.exportTextData(stl.data[0], app.id + ".stl"); - }); app.bus.subscribe("solid-pick", function(solid) { ui.registerWizard(new TransformWizard(app.viewer, solid)); }); @@ -165,20 +157,20 @@ UI.prototype.cutExtrude = function(isCut) { }; UI.prototype.createCraftToolBar = function (vertPos) { - var toolBar = new ToolBar(); - toolBar.add('Edit', 'img/3d/face-edit96.png', () => this.app.sketchFace()); - toolBar.add('Cut', 'img/3d/cut96.png', this.cutExtrude(true)); - toolBar.add('Extrude', 'img/3d/extrude96.png', this.cutExtrude(false)); - 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)); + var toolBar = new ToolBar(this.app); + toolBar.add(this.app.actionManager.actions['EditFace']); + toolBar.add(this.app.actionManager.actions['CUT']); + toolBar.add(this.app.actionManager.actions['PAD']); + toolBar.add(this.app.actionManager.actions['PLANE']); + toolBar.add(this.app.actionManager.actions['BOX']); + toolBar.add(this.app.actionManager.actions['SPHERE']); $('#viewer-container').append(toolBar.node); toolBar.node.css({left: '10px',top : vertPos + 'px'}); return toolBar; }; UI.prototype.createMiscToolBar = function (vertPos) { - var toolBar = new ToolBar(); + var toolBar = new ToolBar(this.app); toolBar.addFa('floppy-o', () => this.app.sketchFace()); toolBar.addFa('upload', () => this.app.sketchFace()); toolBar.addFa('refresh', () => this.app.sketchFace()); @@ -190,10 +182,10 @@ UI.prototype.createMiscToolBar = function (vertPos) { }; UI.prototype.createBoolToolBar = function(vertPos) { - var toolBar = new ToolBar(); - 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)); + var toolBar = new ToolBar(this.app); + toolBar.add(this.app.actionManager.actions['INTERSECTION']); + toolBar.add(this.app.actionManager.actions['DIFFERENCE']); + toolBar.add(this.app.actionManager.actions['UNION']); $('#viewer-container').append(toolBar.node); toolBar.node.css({left: '10px', top : vertPos + 'px'}); return toolBar; @@ -214,9 +206,9 @@ UI.prototype.registerMenuActions = function() { 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('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); @@ -264,6 +256,21 @@ UI.getIconForOp = function(op) { }; +UI.prototype.initOperation = function(op) { + if ('CUT' === op) { + this.cutExtrude(false)(); + } else if ('PAD' === op) { + this.cutExtrude(false)(); + } else if ('BOX' === op) { + this.registerWizard(new BoxWizard(this.app.viewer), false) + } else if ('PLANE' === op) { + this.registerWizard(new PlaneWizard(this.app.viewer), false) + } else if ('SPHERE' === op) { + this.registerWizard(new SphereWizard(this.app.viewer), false) + } else { + console.log('unknown operation'); + } +}; UI.prototype.createWizardForOperation = function(op) { var initParams = op.protoParams; diff --git a/web/app/3d/ui/message-sink.js b/web/app/3d/ui/message-sink.js index 8d8b1b50..de9a0c2f 100644 --- a/web/app/3d/ui/message-sink.js +++ b/web/app/3d/ui/message-sink.js @@ -18,6 +18,7 @@ MessageSink.prototype.hide = function() { MessageSink.prototype.showContent = function(dom) { this.node.children().detach(); + this.node.empty(); this.node.append(dom); this.show(); }; diff --git a/web/app/3d/ui/utils.js b/web/app/3d/ui/utils.js index d237343b..3dbe41f8 100644 --- a/web/app/3d/ui/utils.js +++ b/web/app/3d/ui/utils.js @@ -72,6 +72,11 @@ export function fit(el, relativeEl) { } } +export function capitalize(str) { + if (!str) return; + return str.charAt(0).toUpperCase() + str.slice(1); +} + export function LoadTemplate(name) { return require('./tmpl/' + name + '.html'); } \ No newline at end of file diff --git a/web/app/ui/toolbar.js b/web/app/ui/toolbar.js index ae45eaf1..189e6ce8 100644 --- a/web/app/ui/toolbar.js +++ b/web/app/ui/toolbar.js @@ -1,5 +1,7 @@ +import {capitalize} from 'utils' -export default function ToolBar() { +export default function ToolBar(app) { + this.app = app; this.node = $('
', { css :{ 'position': 'absolute', @@ -10,24 +12,33 @@ export default function ToolBar() { }); } -ToolBar.prototype.add = function(caption, icon, action) { +ToolBar.prototype.add = function(action) { + if (!action) return; var btn = $('
', { - 'class': 'tc-toolbar-btn tc-squeezed-text', - text : caption, + 'class': 'tc-toolbar-btn tc-squeezed-text action-item', + text : capitalize(action.label), css: ToolBar.buttonCss({ - 'background-image': 'url('+icon+')', + 'background-image': 'url('+action.icon96+')', 'background-repeat': 'no-repeat', 'background-position-x': 'center', 'background-position-y': 'top', 'background-size': '48px 48px' }) }); - btn.click(action); + btn.attr('data-action', action.id); + this.app.actionManager.subscribe(action.id, (state) => { + if (state.enabled) { + btn.removeClass('action-disabled'); + } else { + btn.addClass('action-disabled'); + } + }); this.node.append(btn); return btn; }; ToolBar.prototype.addFa = function(faIcon, action) { + if (!action) return; var btn = $('
', { 'class': 'tc-toolbar-btn', css : { diff --git a/web/css/app3d.less b/web/css/app3d.less index 2e6078a8..284a9d4c 100644 --- a/web/css/app3d.less +++ b/web/css/app3d.less @@ -212,7 +212,6 @@ body { display: none; position: absolute; max-width: 400px; - padding: 5px; padding: 2px 5px 2px 5px; .aux-win; color: #ccc; diff --git a/web/css/toolkit.css b/web/css/toolkit.css index ccbf61ab..a045306f 100644 --- a/web/css/toolkit.css +++ b/web/css/toolkit.css @@ -166,4 +166,13 @@ white-space: nowrap; font-size: 12px; font-family: "Arial Narrow", Arial, sans-serif; +} + +.tc-toolbar-btn.action-disabled { + color: #999; +} + +.tc-toolbar-btn.action-disabled:hover { + color: #aaa; + background-color: #888; } \ No newline at end of file