UNPKG

awv3

Version:
644 lines (590 loc) 27.9 kB
import * as THREE from 'three'; import Orbit from '../../controls/orbit'; import { Button, Checkbox, Console, Group, Label, Link, Selection, Slider, Spacer, Divider, } from '../../session/elements'; import { arrayDiff, createObserver } from '../../session/helpers'; import { MaterialSelector } from '../../session/selection/materialselector'; import Plugin from '../../session/plugin'; import { actions as connectionActions } from '../../session/store/connections'; import Object3 from '../../three/object3'; import Dimension from '../dimension/'; import Ccref from './ccref'; import ConstraintGenerator from './constraint/generator'; import * as ConstraintType from './constraint/type'; import ConstraintVisualizer from './constraint/visualizer'; import commandRunner from './command/commandrunner'; import { addCommand, removeCommands, setPlaneCommands } from './command/highlevel'; import { CopyObjects, IdToReal, MoveObjects, Recalc, SolveConstraints, UpdateDimensions, Return, Sequence, } from './command/lowlevel'; import MultiRunner from './command/multirunner'; import Graphics from './graphics'; import Handler from './handlers'; import DuplicateHandler from './handlers/duplicate'; //import help from './help'; const resourcesPaths = { 'help' : 'help.png', 'cancel' : 'cancel.png', 's' : 's.png', 'ac' : 'ac.png', 'duplicate' : 'duplicate.png', 'line' : 'line.png', 'point' : 'point.png', 'arc-center' : 'TODO/arc-center.png', 'arc-3points' : 'arc-3points.png', 'arc-tangential' : 'arc-tangential.png', 'circle-center-radius' : 'circle-center-radius.png', 'fillet-sketch' : 'fillet-sketch.png', 'fixation' : 'fixation.png', 'horizontality' : 'horizontality.png', 'verticality' : 'verticality.png', 'incidence' : 'coincident.png', 'tangency' : 'tangency.png', 'parallelity' : 'parallelity.png', 'perpendicularity' : 'perpendicularity.png', 'colinear' : 'colinear.png', 'concentric' : 'concentric.png', 'midpoint' : 'midpoint.png', 'symmetric' : 'symmetric.png', 'equalDistance' : 'equal.png', 'equalRadius' : 'equal.png', 'horizontalDistance' : 'hdimension.png', 'verticalDistance' : 'vdimension.png', 'distance' : 'dimension.png', 'radius' : 'TODO/radius.png', 'diameter' : 'dimension.png', 'angle' : 'TODO/angle.png', 'angleox' : 'TODO/angleox.png', }; const resources = {}; for (let name in resourcesPaths) { let path = resourcesPaths[name]; resources[name] = require('!!url-loader!awv3-icons/32x32/' + path); } const textures = {}, textureLoader = new THREE.TextureLoader(); for (let [name, url] of Object.entries(resources)) textures[name] = new Promise((resolve, reject) => textureLoader.load(url, resolve, undefined, reject)); export default class Sketcher extends Plugin { static measurable = true; constructor(session, args) { super(session, { type: 'Sketch', icon: 'sketch', resources, ...args }); this.activeSketch = new Ccref(this, this.feature); this.autoconstraintIncremental = true; this.constraintVisualizer = new ConstraintVisualizer(this); this.dimension = new Dimension(this.session, { name: 'Dimensions', recalc: false, collapsed: true, closeable: false, parent: this.id, }); this.dimension.afterSetCallback = this.incrementalSolveConstraints.bind(this); this.dependencies.push(this.dimension); this.graphics = new Map(); this.graphicScale = 0.1; //radius of a sketch point this.gridStep = 0.1; //size of grid cell this.multiRunner = new MultiRunner(commandRunner.bind(undefined, this.connection)); this.onPlaneChange = this.onPlaneChange.bind(this); this.onSketchObjectChange = this.onSketchObjectChange.bind(this); this.onSketchObjectRemove = this.onSketchObjectRemove.bind(this); this.recalcIncremental = false; this.selector = new MaterialSelector(session); this.selector.hoveredProps = { ...this.selector.hoveredProps, polygonOffsetFactor: -10, polygonOffsetUnits: -50, }; this.selector.selectedProps = { ...this.selector.selectedProps, polygonOffsetFactor: -5, polygonOffsetUnits: -25, }; this.dimension.selector = this.selector; this.solveIncremental = false; this.textures = textures; this.transientGeomParams = new Map(); this.addElements(); } addElements() { const GF = Group.Format, BF = Button.Format; this.namedElements = { plane: new Selection(this, { name: 'plane', active: false, types: ['Object', 'Workplane'] }), selection: new Selection(this, { name: 'Selection', types: ['SketcherMesh', 'DimensionHandle'] }), console: new Console(this, { name: 'Console' }), dimension: new Link(this, { name: 'Dimensions', value: this.dimension.id, collapsable: true }), incremental: new Slider(this, { name: 'Increment', value: Math.max(1 * this.solveIncremental, 2 * this.recalcIncremental), max: 2, positions: { 0: 'None', 1: 'Solve', 2: 'Recalc' }, }), constrvis: new Slider(this, { name: 'Constraints', value: this.constraintVisualizer.mode, max: 2, positions: { 0: 'Never', 1: 'Hover', 2: 'Always' }, }), handlers: { drag: new Button(this, { format: BF.Toggle, name: 'drag' }), point: new Button(this, { format: BF.Toggle, name: 'point', icon: 'point' }), line: new Button(this, { format: BF.Toggle, name: 'line', icon: 'line' }), arccenter: new Button(this, { format: BF.Toggle, name: 'arc center', icon: 'arc-center' }), arcmiddle: new Button(this, { format: BF.Toggle, name: 'arc middle', icon: 'arc-3points' }), arctangent: new Button(this, { format: BF.Toggle, name: 'arc tangent', icon: 'arc-tangential' }), circle: new Button(this, { format: BF.Toggle, name: 'circle', icon: 'circle-center-radius' }), fillet: new Button(this, { format: BF.Toggle, name: 'fillet', icon: 'fillet-sketch' }), duplicate: new Button(this, { format: BF.Toggle, name: 'Duplicate', icon: 'duplicate' }), }, constraints: { incidence: new Button(this, { name: 'incidence', icon: 'incidence' }), tangency: new Button(this, { name: 'tangency', icon: 'tangency' }), verticality: new Button(this, { name: 'verticality', icon: 'verticality' }), horizontality: new Button(this, { name: 'horizontality', icon: 'horizontality' }), parallelity: new Button(this, { name: 'parallelity', icon: 'parallelity' }), perpendicularity: new Button(this, { name: 'perpendicularity', icon: 'perpendicularity' }), fixation: new Button(this, { name: 'fixation', icon: 'fixation' }), colinear: new Button(this, { name: 'colinear', icon: 'colinear' }), concentric: new Button(this, { name: 'concentric', icon: 'concentric' }), midpoint: new Button(this, { name: 'midpoint', icon: 'midpoint' }), symmetric: new Button(this, { name: 'symmetric', icon: 'symmetric' }), equalDistance: new Button(this, { name: 'equal length', icon: 'equalDistance' }), equalRadius: new Button(this, { name: 'equal radius', icon: 'equalRadius' }), distance: new Button(this, { name: 'distance', icon: 'distance' }), horizontalDistance: new Button(this, { name: 'horizontal distance', icon: 'horizontalDistance' }), verticalDistance: new Button(this, { name: 'vertical distance', icon: 'verticalDistance' }), radius: new Button(this, { name: 'radius', icon: 'radius' }), diameter: new Button(this, { name: 'diameter', icon: 'diameter' }), angle: new Button(this, { name: 'angle', icon: 'angle' }), angleox: new Button(this, { name: 'angleox', icon: 'angleox' }), }, actions: { solve: new Button(this, { name: 'Solve', icon: 's' }), autoconstr: new Button(this, { name: 'Autoconstr', icon: 'ac' }), delete: new Button(this, { name: 'Delete', icon: 'cancel' }), help: new Button(this, { name: /*help*/ 'Help', icon: 'help' }), }, coordinateShower: { xLabel: new Label(this, { value: 'X', header: true, flex: 'inherit' }), xCoord: new Label(this, { value: 0, flex: 1 }), yLabel: new Label(this, { value: 'Y', header: true, flex: 'inherit' }), yCoord: new Label(this, { value: 0, flex: 1 }), }, }; this.namedElements.arcs = new Button(this, { format: BF.Menu, name: 'arctangent', icon: 'arc-tangential', children: [ new Group(this, { name: 'arc group', format: GF.Buttons, children: [ this.namedElements.handlers.arctangent, this.namedElements.handlers.arccenter, this.namedElements.handlers.arcmiddle, this.namedElements.handlers.circle, ], }), ], }); this.namedElements.toolsGroup = new Group(this, { name: 'Tools', format: GF.Buttons, children: [ this.namedElements.handlers.point, this.namedElements.handlers.line, this.namedElements.arcs, this.namedElements.handlers.fillet, this.namedElements.handlers.duplicate, ], limit: 4, }); this.namedElements.actionsGroup = new Group(this, { name: 'Actions', format: GF.Buttons, children: Object.values(this.namedElements.actions), limit: 4, }); Object.values(this.namedElements.constraints).forEach(item => item.visible = false); this.namedElements.constraintsGroup = new Group(this, { name: 'Constraints', format: GF.Buttons, children: Object.values(this.namedElements.constraints), limit: 4, visible: false, }); this.namedElements.mouseGroup = new Group(this, { name: 'Mouse', format: GF.Rows, children: [ this.namedElements.coordinateShower.xLabel, this.namedElements.coordinateShower.xCoord, this.namedElements.coordinateShower.yLabel, this.namedElements.coordinateShower.yCoord, ], }); this.addElement( new Group(this, { format: GF.Table, children: [ this.namedElements.plane, this.namedElements.incremental, this.namedElements.constrvis, this.namedElements.toolsGroup, this.namedElements.actionsGroup, this.namedElements.constraintsGroup, this.namedElements.console, this.namedElements.mouseGroup, ], }), ); this.setCursorCoordinates(undefined); this.addElement(this.namedElements.dimension); } getSelected() { return this.selector.getSelectedIds().map(id => new Ccref(this, id)); } observeElements() { const value = s => s.value, click = s => s.lastEvent, children = s => s.children; this.namedElements.plane.observe(children, x => this.setPlaneFromSelection()); this.namedElements.selection.observe(children, x => { const entities = this.getSelected(); const items = Object.entries(this.namedElements.constraints); for (let [name, element] of items) element.visible = Boolean(ConstraintType[name].adapt(entities)); this.namedElements.constraintsGroup.visible = items.some(([name, element]) => element.visible); }); this.namedElements.console.observe(click, async event => { if (event.key === 'Enter' && event.target) { await this.activeHandler.consoleExecute(event.target.value); this.namedElements.console.value = ''; this.refresh(); } }); this.namedElements.console.observe(value, async x => { const completions = this.activeHandler.consoleComplete(x); this.namedElements.console.children = completions.filter(c => c.trim() !== ''); this.activeHandler.parseRestrictions(x); this.refresh(); }); this.namedElements.incremental.observe(value, x => { this.solveIncremental = x >= 1; this.recalcIncremental = x >= 2; this.incrementalSolveConstraints(); }); this.namedElements.constrvis.observe(value, x => this.constraintVisualizer.updateAll(x)); this.namedElements.arcs.observe(click, x => { const button = this.namedElements.handlers[this.namedElements.arcs.name]; button.value = !button.value; }); for (let archandler of ['arctangent', 'arccenter', 'arcmiddle', 'circle']) { const arcelement = this.namedElements.handlers[archandler]; arcelement.observe(click, x => { this.namedElements.arcs.name = archandler; this.namedElements.arcs.icon = arcelement.icon; }); } for (let [handler, element] of Object.entries(this.namedElements.handlers)) if (handler !== 'duplicate') element.observe(value, flag => this.onHandlerToggle(handler, flag)); this.namedElements.handlers.duplicate.observe(value, flag => { if (flag) { let objects = this.getSelected(); objects = DuplicateHandler.normalizeSelection(objects); if (objects.length > 0) { this.onHandlerToggle('duplicate', true); this.activeHandler.init(objects); } else this.namedElements.handlers.duplicate.value = false; } else this.onHandlerToggle('duplicate', false); }); for (let [constraint, element] of Object.entries(this.namedElements.constraints)) element.observe(click, async x => { const ct = ConstraintType[constraint]; let entities = ct.adapt(this.getSelected()), value = {}; if (ct.type === 'CC_2DAngleConstraint') { this.activeHandler.removeInteractions(); const oldHandler = this.activeHandler; this.activeHandler = Handler(this, 'angle'); this.activeHandler.setLines(entities); value = await new Promise(resolve => this.activeHandler.resolve = resolve); this.activeHandler.destroy(); this.activeHandler = oldHandler; this.activeHandler.addInteractions(); } if (!entities) return; this.selector.removeAll(); if (!value) return; // cancelled angle let cmd = addCommand(this.activeSketch, { class: ct.type, entities, value }); if (ct.isParametric) cmd = IdToReal(cmd); const dimId = this.run(cmd); this.incrementalSolveConstraints(); }); this.namedElements.actions.solve.observe(click, x => this.solveConstraints()); this.namedElements.actions.autoconstr.observe(click, x => this.run(this.generateConstraints(this.activeSketch))); this.namedElements.actions.delete.observe(click, x => this.deleteSelected()); } onEnabled() { this.session.pool.fadeOut(); this.pool.createInteraction().on({ [Object3.Events.Lifecycle.Rendered]: this.onRender.bind(this), }, {sync: true}); this.observeElements(); this.namedElements.handlers['drag'].value = true; this.dimension.enabled = true; this.connection.observe(state => state.tree[this.activeSketch.id], this.onSketchObjectChange, { fireOnStart: true, unsubscribeOnUndefined: true, onRemove: this.onSketchObjectRemove, manager: this.addSubscription.bind(this), }); this.connection.observe( state => state.tree[this.activeSketch.id].members.planeReference.value, this.onPlaneChange, { fireOnStart: true, manager: this.addSubscription.bind(this), }, ); this.constraintVisualizer.updateAll(); //this.session.pool.view.controls.noRotate = true; this.view.controls.focus().zoom(); this.zoomMode_old = this.view.controls.zoomMode; this.view.controls.zoomMode = Orbit.ZoomMode.Mouse; } onDisabled() { this.session.pool.fadeIn(); // Unlock controls, fade in pool //this.session.pool.view.controls.noRotate = false; this.view.controls.focus().zoom(); this.dimension.enabled = false; if (this.activeHandler) { // disable possible complex handler (polyline) first, then disable drag this.namedElements.handlers[this.activeHandler.name].value = false; this.onHandlerToggleRecursion = true; this.namedElements.handlers['drag'].value = false; this.onHandlerToggleRecursion = false; } this.view.controls.zoomMode = this.zoomMode_old; } onSketchObjectChange(object, oldObject) { const id = object.id, ccref = new Ccref(this, id); // create graphics if (!this.graphics.has(id)) { const g = Graphics(object.class); if (!g) return; this.pool.add(g); this.graphics.set(id, g); this.activeHandler.addInteraction(ccref); this.constraintVisualizer.addConstraint(ccref); } // create new children subscriptions arrayDiff(object.children, (oldObject || {}).children, newChildren => newChildren.forEach(child => this.connection.observe(state => state.tree[child], this.onSketchObjectChange, { fireOnStart: true, unsubscribeOnUndefined: true, onRemove: this.onSketchObjectRemove, manager: this.addSubscription.bind(this), }))); // update graphics ccref.updateGraphics(); // update parent line/arc graphics if this was an endpoint change or similar if (object.id !== this.activeSketch.id && object.parent !== this.activeSketch.id) new Ccref(this, object.parent).updateGraphics(); } onPlaneChange(planeId) { const isCorrect = Boolean(planeId); this.namedElements.incremental.visible = isCorrect; this.namedElements.constrvis.visible = isCorrect; this.namedElements.toolsGroup.visible = isCorrect; this.namedElements.actionsGroup.visible = isCorrect; this.namedElements.constraintsGroup.visible = isCorrect; this.namedElements.console.visible = isCorrect; this.namedElements.mouseGroup.visible = isCorrect; this.namedElements.dimension.visible = isCorrect; this.pool.visible = isCorrect; } onSketchObjectRemove(object, unsubscribe) { const id = object.id; // destroy graphics if (this.graphics.has(id)) { const ccref = new Ccref(this, id); // pass object too because ccref.state can be undefined this.constraintVisualizer.removeConstraint(ccref, object); this.activeHandler.removeInteraction(ccref); this.graphics.get(id).destroy(); this.graphics.delete(id); } // free transient info if (this.transientGeomParams.has(id)) this.transientGeomParams.delete(id); // remove a reference added by addSubscription this.removeSubscription(unsubscribe); } onHandlerToggle(handler, value) { if (this.onHandlerToggleRecursion) return; this.onHandlerToggleRecursion = true; if (value) { if (this.activeHandler) this.namedElements.handlers[this.activeHandler.name].value = false; this.activeHandler = Handler(this, handler, this.activeHandler); } else { this.namedElements.handlers['drag'].value = true; this.activeHandler = Handler(this, 'drag', this.activeHandler); } this.activeHandler.parseRestrictions(this.namedElements.console.value); this.onHandlerToggleRecursion = false; } onRender() { const graphicDirty = this.updateGraphicScale(); if (graphicDirty) this.activeSketch.updateGraphicsRecursive(); this.constraintVisualizer.render(); } updateGraphicScale() { // just take sketch origin as scaling point const globalPnt = new THREE.Vector3(0, 0, 0).applyMatrix4(this.activeSketch.matrixWorld); // get length-to-pixels local scaling at this point const approxScale = this.view.calculateScaleFactor(globalPnt, 7); // adjust to a good number const newScale = Math.pow(10.0, Math.floor(Math.log10(approxScale) * 5) / 5); // set grid step const newStep = Math.pow(10.0, Math.floor(Math.log10(approxScale * 30) + 1e-3)); //change scale and grid atep const changed = newStep !== this.gridStep || newScale !== this.graphicScale; this.gridStep = newStep; this.graphicScale = newScale; return changed; } //only for showing them in GUI setCursorCoordinates(pos) { if (pos) { this.namedElements.coordinateShower.xCoord.visible = true; this.namedElements.coordinateShower.yCoord.visible = true; this.namedElements.coordinateShower.xCoord.value = pos.x; this.namedElements.coordinateShower.yCoord.value = pos.y; } else { this.namedElements.coordinateShower.xCoord.visible = false; this.namedElements.coordinateShower.yCoord.visible = false; } } refresh() { this.view.invalidate(); } run(command) { return this.multiRunner.run({ command: 'Execute', task: Sequence(command).unparse() }); } checkSolveResult(result) { // result: 0 - fail, 1 - well-defined, 2 - solved if (result === 0) { this.namedElements.incremental.error = 'Failed to solve sketch'; this.namedElements.incremental.value = 0; return false; } this.namedElements.incremental.error = undefined; return true; } async solveConstraints() { const result = this.run(Return(SolveConstraints(this.activeSketch))); this.run(UpdateDimensions(this.activeSketch)); return this.checkSolveResult(await result); } recalc() { return this.run(Recalc()); } async incrementalSolveConstraints() { const solvePromise = this.solveIncremental ? this.solveConstraints() : Promise.resolve(); const recalcPromise = this.recalcIncremental ? this.recalc() : Promise.resolve(); await Promise.all([solvePromise, recalcPromise]); } generateConstraints(object) { return new ConstraintGenerator(this.activeSketch).generateImpliedConstraints(object); } autoconstraintCommands(object) { return this.autoconstraintIncremental ? this.generateConstraints(object) : []; } async moveUnderConstraints(objects, delta) { if (delta.lengthSq() === 0) return; if (this.solveInProgress) return; this.solveInProgress = true; // hack: prevent multiple concurrent requests to the server const command = Return(MoveObjects(this.activeSketch, this.noMultiMoveSupport ? objects[0] : objects, delta)); try { const result = await this.run(command); this.checkSolveResult(result); } catch (e) { if ( !this.noMultiMoveSupport && e.errorCode === 0 && e.errorState === 2 && e.errorMessage === '[Evaluationsfehler in SketcherCloudInterop.MoveObjects:[CCVM::callsf: objId not found]] ' ) { console.warn('No support for moving multiple objects, update server classes'); this.noMultiMoveSupport = true; } else throw e; } finally { this.solveInProgress = false; } } copyObjects(objects, translate, rotate) { return this.run(CopyObjects(this.activeSketch, objects, translate, rotate)); } deleteSelected({ onlyIfConsoleIsEmpty } = {}) { if (onlyIfConsoleIsEmpty && this.namedElements.console.value !== '') return; let selected = this.getSelected(); // replace selected dimensions with their master constraints for (let i = 0; i < selected.length; i++) { let ent = selected[i]; if (ent.isDimension()) selected[i] = new Ccref(this, this.connection.tree[ent.id].members.master.value); } selected = selected.filter(ent => ent); if (selected.length !== 0) { // delete sketch objects this.selector.removeAll(); this.run(removeCommands(this.activeSketch, ...selected)); this.incrementalSolveConstraints(); } } setPlaneFromSelection() { const object = this.session.selector.getSelectedElements()[0]; if (object === undefined) return; const plane = new Ccref(this, object.userData.meta.id); this.run(setPlaneCommands(this.activeSketch, plane)); this.namedElements.plane.active = false; this.session.selector.deactivate(); } switchToOrthographicCamera(view, sketch) { view.perspectiveControls = view.controls; view.camera = new THREE.OrthographicCamera(0, 1, 1, 0); sketch.localToWorld(view.camera.position.set(0, 0, 1000)); view.camera.up.transformDirection(sketch.matrixWorld); view.camera.size = 1; view.controls = view.controls.clone(); view.controls.noRotate = true; view.controls.zoomMode = Orbit.ZoomMode.Mouse; view.controls.focus(sketch).zoom(200).now(); } switchToPerspectiveCamera(view) { view.controls = view.perspectiveControls; view.camera = view.controls.camera; view.perspectiveControls = undefined; } }