UNPKG

awv3

Version:
262 lines (229 loc) 9.91 kB
import * as THREE from 'three'; import Object3 from '../../../three/object3'; import Ccref from '../ccref'; import { getArcAngles } from '../geomutils'; import ConstraintGraphics from '../graphics/constraint'; const Mode = Object.freeze({ Never: 0, Hover: 1, Always: 2, }); function uniNorm2(vec) { return Math.max(Math.abs(vec.x), Math.abs(vec.y)); } function sqr(x) { return x * x; } export default class Visualizer { constructor(sketcher, textures) { this.mode = Mode.Hover; this.sketcher = sketcher; // Set of constraint ids to avoid iterating and filtering sketch.children this.constraints = new Set(); // Reverse mapping from entities to constraints this.entityFor = new Map(); // For each constraint, its based entity is stored (where icon is rendered) this.baseEntityFor = new Map(); // If set to true, constraints would be repositioned next frame this.graphicsDirty = false; } refresh() { this.graphicsDirty = true; this.sketcher.refresh(); } isSelected(id) { return this.sketcher.selector.getSelectedIds().indexOf(id) !== -1; } setVisibility(id, newValue = undefined) { if (newValue === undefined) newValue = (this.mode === Mode.Always); //note: we forbid hiding selected constraints if (!newValue && this.isSelected(id)) return; new Ccref(this.sketcher, id).graphics.visible = newValue; } addConstraint(constraint) { if (!(constraint.graphics instanceof ConstraintGraphics)) return; this.constraints.add(constraint.id); this.baseEntityFor.set(constraint.id, undefined); for (let entity of constraint.entities) { if (!this.entityFor.has(entity.id)) this.entityFor.set(entity.id, []); this.entityFor.get(entity.id).push(constraint.id); } this.setVisibility(constraint.id); this.refresh(); } removeConstraint(constraint, state = constraint.state) { if (!(constraint.graphics instanceof ConstraintGraphics)) return; for (let {value: id} of state.members.entities.members) { const constraints = this.entityFor.get(id); constraints.splice(constraints.indexOf(constraint.id), 1); if (constraints.length === 0) this.entityFor.delete(id); } this.constraints.delete(constraint.id); this.baseEntityFor.delete(constraint.id); this.refresh(); } //note: must be called if constraint has changed updateConstraint(constraint) { this.refresh(); } //note: must be called if entity of constraint has changed updateEntity(entity) { if (!this.entityFor.has(entity.id)) return; this.refresh(); } hover(entity, first) { if (this.mode !== Mode.Hover) return; if (entity.isConstraint()) return; if (first) this.unhoverAll(); for (let id of this.entityFor.get(entity.id) || []) { this.setVisibility(id, true); if (!this.isSelected(id)) this.baseEntityFor.set(id, entity.id); } this.refresh(); } unhover(entity, first) { if (this.mode !== Mode.Hover) return; if (entity.isConstraint()) return; // do nothing } unhoverAll() { if (this.mode !== Mode.Hover) return; for (let id of this.constraints.keys()) this.setVisibility(id, false); this.refresh(); } //called when constraint visualizer mode changes + on init updateAll(mode = this.mode) { this.mode = mode; for (let id of this.constraints.keys()) this.setVisibility(id); this.refresh(); } //note: called before each frame is rendered //sets proper positions for all constraint graphics render() { if (!this.graphicsDirty) return; this.graphicsDirty = false; const iconSize = 3 * this.sketcher.graphicScale; //determine for each base entity the array of all constraints drawn near it let perEntityLists = new Map(); for (let conId of this.constraints.keys()) { let constr = new Ccref(this.sketcher, conId); if (!constr.graphics.visible) continue; let baseId = this.getBaseEntityId(conId); if (!perEntityLists.has(baseId)) perEntityLists.set(baseId, []); perEntityLists.get(baseId).push(conId); } if (perEntityLists.size === 0) return; //create rectangle filled with constraints icons for each base entity let rectangles = Array.from(perEntityLists.keys()).map(id => { return { id, keyPos: this.getKeyPosition(id), cnt: perEntityLists.get(id).length, }; }); //sort base entities (for stability) rectangles.sort((a, b) => { return a.id - b.id; }); //minimal distance between rectangles (in iconSize units) const colEps = 0.5; //minimal vertical distance from key position to rectangle's center (in iconSize units) const minOffsetH = 0.75; //set positions for all entity lists (each one is rectangle) for (let i = 0; i < rectangles.length; i++) { let newRect = rectangles[i]; //position of rectangle center, which is closest to key point let bestDistSqr = 1e+50; let bestX, bestY; rectanglePlaceLoop: for (let vk = 0; ; vk++) for (let vs = 1; vs >= -1; vs -= 2) { //try to put into the vertical slice: let cy = newRect.keyPos.y + vs * (minOffsetH + vk * (1 + colEps)) * iconSize; //loop termination criterion if (sqr(cy - newRect.keyPos.y) >= bestDistSqr) break rectanglePlaceLoop; //try both direction for horizontal moving for (let hs = 1; hs >= -1; hs -= 2) { let cx = newRect.keyPos.x; do { var moved = false; for (let j = 0; j < i; j++) { let obstRect = rectangles[j]; //check if rectangles collide if (Math.abs(cy - obstRect.ctrPos.y) >= (1 + colEps) * iconSize - 1e-9) continue; if (Math.abs(cx - obstRect.ctrPos.x) >= ((newRect.cnt + obstRect.cnt) * 0.5 + colEps) * iconSize - 1e-9) continue; //do minimal horizontal movement which avoids collision let newSide = cx + hs * iconSize * newRect.cnt * 0.5; let obstSide = obstRect.ctrPos.x - hs * iconSize * obstRect.cnt * 0.5; let diff = (newSide - obstSide) + hs * colEps * iconSize; cx += diff; moved = true; } //note: if something has moved, then we have to recheck everything for collisions } while (moved); let distSqr = sqr(cx - newRect.keyPos.x) + sqr(cy - newRect.keyPos.y); if (bestDistSqr > distSqr) { bestDistSqr = distSqr; bestX = cx; bestY = cy; } //early cutoff: if no horizontal movement happened if (cx === newRect.keyPos.x) break rectanglePlaceLoop; } } //place rectangle into found position (not changed later) newRect.ctrPos = new THREE.Vector3(bestX, bestY, 0.0); } //set positions for all constraint icons for (let {id, ctrPos, cnt} of rectangles) { let conList = perEntityLists.get(id); //sort constraint lists (for stability) conList.sort((a, b) => (a - b)); for (let i = 0; i < cnt; i++) { let pos = ctrPos.clone().add(new THREE.Vector3(i - 0.5 * (cnt-1), 0, 0).multiplyScalar(iconSize)); let conId = conList[i]; let constr = new Ccref(this.sketcher, conId); constr.graphics.localMesh.position.copy(pos); constr.graphics.localMesh.scale.set(iconSize, iconSize, 1); } } } getKeyPosition(entity) { if (typeof(entity) === "number") entity = new Ccref(this.sketcher, entity); const gp = entity.geomParams; switch (entity.class) { case 'CC_Point': return gp.start.clone(); case 'CC_Line': return new THREE.Vector3().lerpVectors(gp.start, gp.end, 0.5); case 'CC_Arc': return getArcAngles(gp).mid.clone(); case 'CC_Circle': return new THREE.Vector3(0.6, 0.8).multiplyScalar(gp.radius).add(gp.center); default: return new THREE.Vector3(0, 0, 0); } } getBaseEntityId(constraint) { if (typeof(constraint) === "number") constraint = new Ccref(this.sketcher, constraint); let res = this.baseEntityFor.get(constraint.id); if (res !== undefined) return res; for (let entity of constraint.entities) if (['CC_Point', 'CC_Line', 'CC_Arc', 'CC_Circle'].indexOf(entity.class) !== -1) return entity.id; return constraint.entities[0].id; } }