awv3
Version:
⚡ AWV3 embedded CAD
644 lines (590 loc) • 27.9 kB
JavaScript
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;
}
}