UNPKG

@xeokit/xeokit-sdk

Version:

3D BIM IFC Viewer SDK for AEC engineering applications. Open Source JavaScript Toolkit based on pure WebGL for top performance, real-world coordinates and full double precision

600 lines (528 loc) 23.5 kB
import {math} from "../../viewer/scene/math/math.js"; import {buildCylinderGeometry} from "../../viewer/scene/geometry/builders/buildCylinderGeometry.js"; import {buildTorusGeometry} from "../../viewer/scene/geometry/builders/buildTorusGeometry.js"; import {ReadableGeometry} from "../../viewer/scene/geometry/ReadableGeometry.js"; import {PhongMaterial} from "../../viewer/scene/materials/PhongMaterial.js"; import {EmphasisMaterial} from "../../viewer/scene/materials/EmphasisMaterial.js"; import {Node} from "../../viewer/scene/nodes/Node.js"; import {Mesh} from "../../viewer/scene/mesh/Mesh.js"; import {buildSphereGeometry} from "../../viewer/scene/geometry/builders/buildSphereGeometry.js"; import {worldToRTCPos} from "../../viewer/scene/math/rtcCoords.js"; import {transformToNode} from "../lib/ui/index.js"; const zeroVec = new Float64Array([0, 0, 1]); const quat = new Float64Array(4); /** * Controls a transformation with mouse and touch input. * * @private */ class TransformControl { /** @private */ constructor(viewer) { const cameraControl = viewer.cameraControl; const scene = viewer.scene; const canvas = scene.canvas.canvas; // Builds the Entities that represent this Control. const NO_STATE_INHERIT = false; const arrowLength = 1.0; const handleRadius = 0.09; const tubeRadius = 0.01; const rootNode = new Node(scene, { // Root of Node graph that represents this control in the 3D scene position: [0, 0, 0], scale: [5, 5, 5], isObject: false }); const pos = math.vec3(); this._setPosition = (function() { const origin = math.vec3(); const rtcPos = math.vec3(); return function(p) { pos.set(p); worldToRTCPos(p, origin, rtcPos); rootNode.origin = origin; rootNode.position = rtcPos; }; })(); this._setQuaternion = q => { rootNode.quaternion = q; }; const arrowGeometry = (radiusBottom, height) => new ReadableGeometry(rootNode, buildCylinderGeometry({ radiusTop: 0.001, radiusBottom: radiusBottom, radialSegments: 32, heightSegments: 1, height: height, openEnded: false })); const tubeGeometry = (radius, height, radialSegments) => new ReadableGeometry(rootNode, buildCylinderGeometry({ radiusTop: radius, radiusBottom: radius, radialSegments: radialSegments, heightSegments: 1, height: height, openEnded: false })); const torusGeometry = (tube, arcFraction, tubeSegments) => new ReadableGeometry(rootNode, buildTorusGeometry({ radius: arrowLength - 0.2, tube: tube, radialSegments: 64, tubeSegments: tubeSegments, arc: (Math.PI * 2.0) * arcFraction })); const shapes = {// Reusable geometries curve: torusGeometry(tubeRadius, 0.25, 14), curveHandle: torusGeometry(handleRadius, 0.25, 14), hoop: torusGeometry(tubeRadius, 1, 8), arrowHead: arrowGeometry(0.07, 0.2), arrowHeadBig: arrowGeometry(0.09, 0.25), arrowHeadHandle: tubeGeometry(handleRadius, 0.37, 8), axis: tubeGeometry(tubeRadius, arrowLength, 20), axisHandle: tubeGeometry(handleRadius, arrowLength, 20) }; const colorMaterial = (rgb) => new PhongMaterial(rootNode, { diffuse: rgb, emissive: rgb, ambient: [0.0, 0.0, 0.0], specular: [.6, .6, .3], shininess: 80, lineWidth: 2 }); const highlightMaterial = (rgb, fillAlpha) => new EmphasisMaterial(rootNode, { edges: false, fill: true, fillColor: rgb, fillAlpha: fillAlpha }); const pickableMaterial = new PhongMaterial(rootNode, { // Invisible material for pickable handles, which define a pickable 3D area diffuse: [1, 1, 0], alpha: 0, // Invisible alphaMode: "blend" }); const handlers = { }; const addAxis = (rgb, hoopRot) => { const axisDirection = math.mulVec3Scalar(rgb, -1, math.vec3()); const material = colorMaterial(rgb); const hoopMatrix = math.quaternionToRotationMat4(hoopRot, math.identityMat4()); math.mulMat4(math.rotationMat4v(Math.PI, [0,1,0], math.mat4()), hoopMatrix, hoopMatrix); const scale = math.scaleMat4v([0.6, 0.6, 0.6], math.identityMat4()); const scaledArrowMatrix = (t, matR) => { const matT = math.translateMat4v(t, math.identityMat4()); const ret = math.identityMat4(); math.mulMat4(matT, matR, ret); math.mulMat4(hoopMatrix, ret, ret); return math.mulMat4(ret, scale, math.identityMat4()); }; const curve = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.curve, material: material, matrix: hoopMatrix, pickable: false, collidable: true, clippable: false, backfaces: true, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const rotateHandle = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.curveHandle, material: pickableMaterial, matrix: hoopMatrix, pickable: true, collidable: true, clippable: false, backfaces: true, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const arrow1 = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.arrowHead, material: material, matrix: scaledArrowMatrix([ .8, .07, 0 ], math.rotationMat4v(180 * math.DEGTORAD, [0, 0, 1], math.identityMat4())), pickable: true, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const arrow2 = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.arrowHead, material: material, matrix: scaledArrowMatrix([ .07, .8, 0 ], math.rotationMat4v(90 * math.DEGTORAD, [0, 0, 1], math.identityMat4())), pickable: true, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const hoop = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.hoop, material: material, highlighted: true, highlightMaterial: highlightMaterial(rgb, 0.6), matrix: hoopMatrix, pickable: false, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const axisRotation = math.quaternionToRotationMat4(math.vec3PairToQuaternion([ 0, 1, 0 ], rgb), math.identityMat4()); math.mulMat4(math.rotationMat4v(Math.PI, [0,1,0], math.mat4()), axisRotation, axisRotation); const translatedAxisMatrix = (yOffset) => math.mulMat4(axisRotation, math.translateMat4c(0, yOffset, 0, math.identityMat4()), math.identityMat4()); const arrowMatrix = translatedAxisMatrix(arrowLength + .1); const shaftMatrix = translatedAxisMatrix(arrowLength / 2); const arrow = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.arrowHead, material: material, matrix: arrowMatrix, pickable: false, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const arrowHandle = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.arrowHeadHandle, material: pickableMaterial, matrix: arrowMatrix, pickable: true, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const shaft = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.axis, material: material, matrix: shaftMatrix, pickable: false, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const shaftHandle = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.axisHandle, material: pickableMaterial, matrix: shaftMatrix, pickable: true, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const bigArrowHead = rootNode.addChild(new Mesh(rootNode, { geometry: shapes.arrowHeadBig, material: material, matrix: arrowMatrix, pickable: false, collidable: true, clippable: false, visible: false, isUI: true, isObject: false }), NO_STATE_INHERIT); const localToWorldVec = (localVec, worldVec) => math.vec3ApplyQuaternion(rootNode.quaternion, localVec, worldVec); const closestPointOnAxis = (function() { const worldAxis = math.vec3(); const org = math.vec3(); const dir = math.vec3(); return (canvasPos, dst) => { localToWorldVec(rgb, worldAxis); const P = pos; const D = worldAxis; math.canvasPosToWorldRay(canvas, scene.camera.viewMatrix, scene.camera.projMatrix, scene.camera.projection, canvasPos, org, dir); const d01 = math.dotVec3(D, dir); const v = math.subVec3(org, P, dst); const v0 = math.dotVec3(v, D); const v1 = math.dotVec3(v, dir); const det = 1 - d01 * d01; if (Math.abs(det) > 1e-10) { // if lines are not parallel const s = (v0 - d01 * v1) / det; math.addVec3(P, math.mulVec3Scalar(D, s, dst), dst); return true; } else { return false; } }; })(); const initOffset = math.vec3(); const tempVec3 = math.vec3(); handlers[arrowHandle.id] = handlers[shaftHandle.id] = { setActivated: a => bigArrowHead.visible = a, initDragAction: (initCanvasPos) => { return closestPointOnAxis(initCanvasPos, initOffset) && math.subVec3(initOffset, pos, initOffset) && ((canvasPos) => { if (closestPointOnAxis(canvasPos, tempVec3)) { math.subVec3(tempVec3, initOffset, tempVec3); this._setPosition(tempVec3); if (this._handlers) { this._handlers.onPosition(tempVec3); } } }); } }; handlers[rotateHandle.id] = { setActivated: a => hoop.visible = a, initDragAction: (initCanvasPos) => { const rotationFromCanvasPos = (function() { const planeCanvasPos = scene.camera.projectWorldPos(pos); localToWorldVec(rgb, tempVec3); math.transformVec3(scene.camera.normalMatrix, tempVec3, tempVec3); const axisCoeff = Math.sign(tempVec3[2]); return (canvasPos) => { const dx = canvasPos[0] - planeCanvasPos[0]; const dy = canvasPos[1] - planeCanvasPos[1]; return axisCoeff * Math.atan2(-dy, dx); }; })(); let lastRotation = rotationFromCanvasPos(initCanvasPos); return canvasPos => { const rotation = rotationFromCanvasPos(canvasPos); rootNode.rotate(rgb, (rotation - lastRotation) * 180 / Math.PI); if (this._handlers) { this._handlers.onQuaternion(rootNode.quaternion); } lastRotation = rotation; }; } }; let positionActive = false; let rotationActive = false; let visible = false; const updatePositionHandle = () => { const v = visible && positionActive; arrowHandle.visible = shaftHandle.visible = arrow.visible = shaft.visible = !!v; if (! v) { bigArrowHead.visible = false; } }; const updateRotationHandle = () => { const v = visible && rotationActive; rotateHandle.visible = curve.visible = arrow1.visible = arrow2.visible = !!v; if (! v) { hoop.visible = false; } }; return { setPositionActive: (a) => { positionActive = a; updatePositionHandle(); }, setRotationActive: (a) => { rotationActive = a; updateRotationHandle(); }, set visible(v) { visible = v; updatePositionHandle(); updateRotationHandle(); } }; }; this._displayMeshes = { center: rootNode.addChild(new Mesh(rootNode, { // center geometry: new ReadableGeometry(rootNode, buildSphereGeometry({ radius: 0.05 })), material: new PhongMaterial(rootNode, { diffuse: [0.0, 0.0, 0.0], emissive: [0, 0, 0], ambient: [0.0, 0.0, 0.0], specular: [.6, .6, .3], shininess: 80 }), pickable: false, collidable: true, clippable: false, visible: false, isObject: false }), NO_STATE_INHERIT), x: addAxis([1,0,0], math.vec3PairToQuaternion([1,0,0], [0,0,1])), y: addAxis([0,1,0], math.eulerToQuaternion([90,0,0], "XYZ")), z: addAxis([0,0,1], math.identityQuaternion()) }; const cleanups = [ ]; { // Keep gizmo screen size constant let lastDist = -1; const setRootNodeScale = size => { if (rootNode.scale[0] !== size) { rootNode.scale = [size, size, size]; if (this._handlers && this._handlers.onScreenScale) { this._handlers.onScreenScale(rootNode.scale); } } }; const onSceneTick = scene.on("tick", () => { const camera = scene.camera; const dist = Math.abs(math.distVec3(camera.eye, pos)); if (camera.projection === "perspective") { if (dist !== lastDist) { setRootNodeScale(0.07 * dist * Math.tan(camera.perspective.fov * math.DEGTORAD)); } } else if (camera.projection === "ortho") { setRootNodeScale(camera.ortho.scale / 10); } lastDist = dist; }); cleanups.push(() => scene.off(onSceneTick)); } { let deactivateActive = null; let currentDrag = null; cleanups.push(() => { if (currentDrag) { currentDrag.cleanup(); } }); const canvasPos = math.vec2(); const copyCanvasPos = (event, vec2) => { vec2[0] = event.pageX; vec2[1] = event.pageY; transformToNode(canvas.ownerDocument.documentElement, canvas, vec2); }; const pickIds = Object.keys(handlers); const pickHandler = (e) => { copyCanvasPos(e, canvasPos); // This doesn't guarantee the gizmo is prioritized (as it should be) by other Scene::pick calls const pickResult = scene.pick({ canvasPos: canvasPos });//, includeEntities: pickIds }); const pickEntity = pickResult && pickResult.entity; const pickId = pickEntity && pickEntity.id; return (pickId in handlers) && handlers[pickId]; }; const startDrag = (event, matchesEvent) => { const handler = pickHandler(matchesEvent(event)); if (handler) { if (currentDrag) { currentDrag.cleanup(); } const dragAction = handler.initDragAction(canvasPos); if (dragAction) { cameraControl.pointerEnabled = false; // or .active = false ? handler.setActivated(true); currentDrag = { onChange: event => { const e = matchesEvent(event); if (e) { copyCanvasPos(e, canvasPos); dragAction(canvasPos); } }, cleanup: function() { currentDrag = null; cameraControl.pointerEnabled = true; // or .active = true ? handler.setActivated(false); } }; } return !!dragAction; } else { return false; } }; const addCanvasEventListener = (type, listener) => { canvas.addEventListener(type, listener); cleanups.push(() => canvas.removeEventListener(type, listener)); }; const preventDefaultStopPropagation = e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); }; addCanvasEventListener("mousedown", (e) => { if ((e.which === 1) && startDrag(e, event => (event.which === 1) && event)) { preventDefaultStopPropagation(e); } }); addCanvasEventListener("mousemove", (e) => { if (currentDrag) { currentDrag.onChange(e); preventDefaultStopPropagation(e); } else { if (deactivateActive) { deactivateActive(); } const handler = pickHandler(e); if (handler) { handler.setActivated(true); deactivateActive = () => handler.setActivated(false); } else { deactivateActive = null; } } }); addCanvasEventListener("mouseup", (e) => { if (currentDrag) { currentDrag.onChange(e); currentDrag.cleanup(); // Calling preventDefaultStopPropagation would interfere with cameraControl // by making it follow the pointer until next click // preventDefaultStopPropagation(e); } }); addCanvasEventListener("touchstart", event => { event.preventDefault(); if (event.touches.length === 1) { const touchStartId = event.touches[0].identifier; if (startDrag(event, event => [...event.changedTouches].find(e => e.identifier === touchStartId))) { preventDefaultStopPropagation(event); } } }); addCanvasEventListener("touchmove", event => { if (currentDrag) { currentDrag.onChange(event); preventDefaultStopPropagation(event); } }); addCanvasEventListener("touchend", event => { if (currentDrag) { currentDrag.onChange(event); currentDrag.cleanup(); // See the comment in mouseup listener // preventDefaultStopPropagation(e); } }); } this.__destroy = () => { cleanups.forEach(c => c()); rootNode.destroy(); for (let id in handlers) { delete handlers[id]; } }; } destroy() { this.__destroy(); } /** * Called to assign this Control to Handlers. * Call with a null or undefined value to disconnect the Control from whatever Handlers it was assigned to. * @private */ setHandlers(handlers) { Object.values(this._displayMeshes).forEach(m => m.visible = !!handlers); this._handlers = handlers; this._displayMeshes.x.setPositionActive(handlers && handlers.onPosition); this._displayMeshes.y.setPositionActive(handlers && handlers.onPosition); this._displayMeshes.z.setPositionActive(handlers && handlers.onPosition); this._displayMeshes.x.setRotationActive(handlers && handlers.onQuaternion); this._displayMeshes.y.setRotationActive(handlers && handlers.onQuaternion); this._displayMeshes.z.setRotationActive(handlers && handlers.onQuaternion); } /** * Sets the World-space position of this Control. * * @param {Number[]} value New position. */ setPosition(p) { this._setPosition(p); } /** * Sets the quaternion of this Control. * * @param {Number[]} value New quaternion. */ setQuaternion(q) { this._setQuaternion(q); } } export {TransformControl};