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