UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

492 lines 18.5 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { BackSide, CanvasTexture, Clock, Color, Euler, LinearFilter, Mesh, MeshBasicMaterial, Object3D, OrthographicCamera, Quaternion, Raycaster, RepeatWrapping, SphereGeometry, Sprite, SpriteMaterial, SRGBColorSpace, Vector2, Vector3, Vector4, WebGLRenderer, } from 'three'; import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'; import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js'; import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'; import { onChangeDispatchEvent } from 'ts-browser-helpers'; const [POS_X, POS_Y, POS_Z, NEG_X, NEG_Y, NEG_Z] = Array(6) .fill(0) .map((_, i) => i); const axesColors = [ new Color(0xff3653), new Color(0x8adb00), new Color(0x2c8fff), ]; const clock = new Clock(); const targetPosition = new Vector3(); const targetQuaternion = new Quaternion(); // const euler = new Euler() const q1 = new Quaternion(); const q2 = new Quaternion(); const point = new Vector3(); // const dim = 128 const turnRate = 2 * Math.PI; // turn rate in angles per second const raycaster = new Raycaster(); const mouse = new Vector2(); // const mouseStart = new Vector2() // const mouseAngle = new Vector2() const dummy = new Object3D(); let radius = 0; /** * Extended ViewHelper implemented from the following source: * https://github.com/Fennec-hub/viewHelper * MIT License * Copyright (c) 2022 Fennec-hub */ export class ViewHelper2 extends Object3D { constructor(camera, canvas, placement = 'bottom-right', size = 128, pixelRatio = 2) { super(); this.orthoCamera = new OrthographicCamera(-1.8, 1.8, 1.8, -1.8, 0, 4); this.isViewHelper = true; this.animating = false; this.target = new Vector3(); // controls?: OrbitControls | TrackballControls // controlsChangeEvent: {listener: () => void} this.viewport = new Vector4(); this.offsetHeight = 0; this.renderer = new WebGLRenderer({ canvas: document.createElement('canvas'), alpha: true, antialias: true, preserveDrawingBuffer: false, }); this.renderer.setPixelRatio(pixelRatio); this.camera = camera; this.domElement = canvas; this.orthoCamera.position.set(0, 0, 2); this.backgroundSphere = getBackgroundSphere(); this.axesLines = getAxesLines(); this.spritePoints = getAxesSpritePoints(); this.add(this.backgroundSphere, this.axesLines, ...this.spritePoints); this.domContainer = getDomContainer(placement, size); this.domContainer.appendChild(this.renderer.domElement); this.renderer.domElement.style.width = '100%'; this.renderer.domElement.style.height = '100%'; // This may cause confusion if the parent isn't the body and doesn't have a `position:relative` this.domElement.parentElement.appendChild(this.domContainer); this.domRect = this.domContainer.getBoundingClientRect(); this.startListening(); // this.controlsChangeEvent = {listener: () => this.updateOrientation()} this.update(); this.updateOrientation(); } startListening() { // this.domContainer.onpointerdown = (e) => this.onPointerDown(e) this.domContainer.onpointermove = (e) => this.onPointerMove(e); this.domContainer.onpointerleave = (e) => this.onPointerLeave(e); this.domContainer.onclick = (e) => this.handleClick(e); } // onPointerDown(e: PointerEvent) { // const drag = (e1: PointerEvent) => { // if (!this.dragging && isClick(e1, mouseStart)) return // if (!this.dragging) { // resetSprites(this.spritePoints) // this.dragging = true // } // // mouseAngle // .set(e1.clientX, e1.clientY) // .sub(mouseStart) // .multiplyScalar(1 / this.domRect.width * Math.PI) // // this.rotation.x = MathUtils.clamp( // rotationStart.x + mouseAngle.y, // Math.PI / -2 + 0.001, // Math.PI / 2 - 0.001 // ) // this.rotation.y = rotationStart.y + mouseAngle.x // this.updateMatrixWorld() // // q1.copy(this.quaternion).invert() // // this.camera.position // .set(0, 0, 1) // .applyQuaternion(q1) // .multiplyScalar(radius) // .add(this.target) // // this.camera.rotation.setFromQuaternion(q1) // // this.updateOrientation(false) // } // const endDrag = () => { // document.removeEventListener('pointermove', drag, false) // document.removeEventListener('pointerup', endDrag, false) // // if (!this.dragging) { // // this.handleClick(e) // return // } // // this.dragging = false // } // // if (this.animating === true) return // e.preventDefault() // // mouseStart.set(e.clientX, e.clientY) // // const rotationStart = euler.copy(this.rotation) // // setRadius(this.camera, this.target) // // document.addEventListener('pointermove', drag, false) // document.addEventListener('pointerup', endDrag, false) // } onPointerMove(e) { // if (this.dragging) return; this.backgroundSphere.material.opacity = 0.4; this.handleHover(e); this.dispatchEvent({ type: 'update', event: e }); } onPointerLeave(e) { // if (this.dragging) return; this.backgroundSphere.material.opacity = 0.2; resetSprites(this.spritePoints); this.domContainer.style.cursor = ''; this.dispatchEvent({ type: 'update', event: e }); } handleClick(e) { const object = getIntersectionObject(e, this.domRect, this.orthoCamera, this.spritePoints); if (!object) return; this.setOrientation(object.userData.type); } handleHover(e) { const object = getIntersectionObject(e, this.domRect, this.orthoCamera, this.spritePoints); resetSprites(this.spritePoints); if (!object) { this.domContainer.style.cursor = ''; } else { object.material.map.offset.x = 0.5; object.scale.multiplyScalar(1.2); this.domContainer.style.cursor = 'pointer'; } } // setControls(controls?: OrbitControls | TrackballControls) { // if (this.controls) { // (this.controls as any).removeEventListener( // 'change', // this.controlsChangeEvent.listener // ) // this.target = new Vector3() // } // // if (!controls) return // // this.controls = controls; // (controls as any).addEventListener('change', this.controlsChangeEvent.listener) // this.target = controls.target // } render() { const delta = clock.getDelta(); if (this.animating) this.animate(Math.min(delta, 1 / 30.0)); // const x = this.domRect.left // const y = this.offsetHeight - this.domRect.bottom const autoClear = this.renderer.autoClear; this.renderer.autoClear = false; // this.renderer.setViewport(x, y, dim, dim) this.renderer.render(this, this.orthoCamera); // this.renderer.setViewport(this.viewport) this.renderer.autoClear = autoClear; } updateOrientation(fromCamera = true) { if (fromCamera) { this.quaternion.copy(this.camera.quaternion).invert(); this.updateMatrixWorld(); } updateSpritesOpacity(this.spritePoints, this.camera); } update() { this.domRect = this.domContainer.getBoundingClientRect(); this.offsetHeight = this.domElement.offsetHeight; setRadius(this.camera, this.target); this.renderer.getViewport(this.viewport); this.updateOrientation(); } animate(delta) { const step = delta * turnRate; // animate position by doing a slerp and then scaling the position on the unit sphere q1.rotateTowards(q2, step); this.camera.position .set(0, 0, 1) .applyQuaternion(q1) .multiplyScalar(radius) .add(this.target); // animate orientation this.camera.quaternion.rotateTowards(targetQuaternion, step); this.updateOrientation(); if (q1.angleTo(q2) === 0) { this.animating = false; } } setOrientation(orientation) { prepareAnimationData(this.camera, this.target, orientation); this.animating = true; this.dispatchEvent({ type: 'update' }); } dispose() { this.axesLines.geometry.dispose(); this.axesLines.material.dispose(); this.backgroundSphere.geometry.dispose(); this.backgroundSphere.material.dispose(); this.spritePoints.forEach((sprite) => { sprite.material.map.dispose(); sprite.material.dispose(); }); this.domContainer.remove(); // ;(this.controls as any)?.removeEventListener( // 'change', // this.controlsChangeEvent.listener // ) } } __decorate([ onChangeDispatchEvent() ], ViewHelper2.prototype, "animating", void 0); function getDomContainer(placement, size) { const div = document.createElement('div'); const style = div.style; style.height = `${size}px`; style.width = `${size}px`; style.borderRadius = '100%'; style.position = 'absolute'; const [y, x] = placement.split('-'); style.transform = ''; style.left = x === 'left' ? '0' : x === 'center' ? '50%' : ''; style.right = x === 'right' ? '0' : ''; style.transform += x === 'center' ? 'translateX(-50%)' : ''; style.top = y === 'top' ? '0' : y === 'bottom' ? '' : '50%'; style.bottom = y === 'bottom' ? '0' : ''; style.transform += y === 'center' ? 'translateY(-50%)' : ''; return div; } function getAxesLines() { const distance = 0.9; const position = Array(3) .fill(0) .map((_, i) => [ !i ? distance : 0, i === 1 ? distance : 0, i === 2 ? distance : 0, 0, 0, 0, ]) .flat(); const color = Array(6) .fill(0) .map((_, i) => i < 2 ? axesColors[0].toArray() : i < 4 ? axesColors[1].toArray() : axesColors[2].toArray()) .flat(); // const geometry = new BufferGeometry() // geometry.setAttribute( // 'position', // new BufferAttribute(new Float32Array(position), 3) // ) // geometry.setAttribute( // 'color', // new BufferAttribute(new Float32Array(color), 3) // ) const geometry = new LineSegmentsGeometry(); geometry.setPositions(position); geometry.setColors(color); return new LineSegments2(geometry, new LineMaterial({ linewidth: 0.02, vertexColors: true, })); } function getBackgroundSphere() { const geometry = new SphereGeometry(1.6); const sphere = new Mesh(geometry, new MeshBasicMaterial({ color: 0xffffff, side: BackSide, transparent: true, opacity: 0.2, depthTest: false, })); return sphere; } function getAxesSpritePoints() { const axes = ['x', 'y', 'z']; return Array(6) .fill(0) .map((_, i) => { const isPositive = i < 3; const sign = isPositive ? '+' : '-'; const axis = axes[i % 3]; const color = axesColors[i % 3]; const sprite = new Sprite(getSpriteMaterial(color, isPositive ? axis : null)); sprite.userData.type = `${sign}${axis}`; sprite.scale.setScalar(isPositive ? 0.6 : 0.4); sprite.position[axis] = isPositive ? 1.2 : -1.2; sprite.renderOrder = 1; return sprite; }); } function getSpriteMaterial(color, text = null) { const canvas = document.createElement('canvas'); const padding = 0; const scale = 1; const padding2 = 0; // has a bug canvas.width = 128 * scale + 4 * padding + padding2 * 2; canvas.height = 64 * scale + 2 * padding + padding2 * 2; const context = canvas.getContext('2d', { alpha: true }); context.beginPath(); context.arc(32 * scale + padding, 32 * scale + padding, 32 * scale - padding, 0, 2 * Math.PI); context.closePath(); context.fillStyle = color.getStyle(); context.fill(); // for black border due to interpolation, transparent slightly bigger circle context.beginPath(); context.arc(32 * scale + padding, 32 * scale + padding, 35 * scale - padding, 0, 2 * Math.PI); context.closePath(); context.fillStyle = '#' + color.getHexString() + '01'; context.fill(); context.beginPath(); context.arc(96 * scale + padding * 3 + padding2, 32 * scale + padding + padding2, 32 * scale - padding - padding2, 0, 2 * Math.PI); context.closePath(); context.fillStyle = '#FFF'; context.fill(); // for black border due to interpolation, transparent slightly bigger circle context.beginPath(); context.arc(96 * scale + padding * 3 + padding2, 32 * scale + padding + padding2, 35 + scale - padding - padding2, 0, 2 * Math.PI); context.closePath(); context.fillStyle = '#FFFFFF01'; context.fill(); if (text !== null) { context.font = 'bold calc(44px * ' + scale + ') Arial'; context.textAlign = 'center'; context.fillStyle = '#111'; context.fillText(text.toUpperCase(), 32 * scale + padding, 48 * scale + padding); context.fillText(text.toUpperCase(), 96 * scale + padding * 3 + padding2, 48 * scale + padding + padding2); } // canvas.style.background = '#ff0000' const texture = new CanvasTexture(canvas); texture.wrapS = texture.wrapT = RepeatWrapping; texture.repeat.x = 0.5; texture.colorSpace = SRGBColorSpace; texture.minFilter = LinearFilter; texture.magFilter = LinearFilter; texture.generateMipmaps = false; texture.needsUpdate = true; return new SpriteMaterial({ map: texture, toneMapped: false, transparent: true, }); } function prepareAnimationData(camera, focusPoint, axis) { switch (axis) { case '+x': targetPosition.set(1, 0, 0); targetQuaternion.setFromEuler(new Euler(0, Math.PI * 0.5, 0)); break; case '+y': targetPosition.set(0, 1, 0); targetQuaternion.setFromEuler(new Euler(-Math.PI * 0.5, 0, 0)); break; case '+z': targetPosition.set(0, 0, 1); targetQuaternion.setFromEuler(new Euler()); break; case '-x': targetPosition.set(-1, 0, 0); targetQuaternion.setFromEuler(new Euler(0, -Math.PI * 0.5, 0)); break; case '-y': targetPosition.set(0, -1, 0); targetQuaternion.setFromEuler(new Euler(Math.PI * 0.5, 0, 0)); break; case '-z': targetPosition.set(0, 0, -1); targetQuaternion.setFromEuler(new Euler(0, Math.PI, 0)); break; default: console.error('ViewHelper: Invalid axis.'); } setRadius(camera, focusPoint); prepareQuaternions(camera, focusPoint); } function setRadius(camera, focusPoint) { radius = camera.position.distanceTo(focusPoint); } function prepareQuaternions(camera, focusPoint) { targetPosition.multiplyScalar(radius).add(focusPoint); dummy.position.copy(focusPoint); dummy.lookAt(camera.position); q1.copy(dummy.quaternion); dummy.lookAt(targetPosition); q2.copy(dummy.quaternion); } function updatePointer(e, domRect, orthoCamera) { mouse.x = (e.clientX - domRect.left) / domRect.width * 2 - 1; mouse.y = -((e.clientY - domRect.top) / domRect.height) * 2 + 1; raycaster.setFromCamera(mouse, orthoCamera); } // function isClick( // e: PointerEvent, // startCoords: Vector2, // threshold = 2 // ) { // return ( // Math.abs(e.clientX - startCoords.x) < threshold && // Math.abs(e.clientY - startCoords.y) < threshold // ) // } function getIntersectionObject(event, domRect, orthoCamera, intersectionObjects) { updatePointer(event, domRect, orthoCamera); const intersects = raycaster.intersectObjects(intersectionObjects); if (!intersects.length) return null; const intersection = intersects[0]; return intersection.object; } function resetSprites(sprites) { let i = sprites.length; while (i--) { const scale = i < 3 ? 0.6 : 0.4; sprites[i].scale.set(scale, scale, scale); sprites[i].material.map.offset.x = 1; } // sprites.forEach((sprite) => (sprite.material.map!.offset.x = 1)); } function updateSpritesOpacity(sprites, camera) { point.set(0, 0, 1); point.applyQuaternion(camera.quaternion); if (point.x >= 0) { sprites[POS_X].material.opacity = 1; sprites[NEG_X].material.opacity = 0.5; } else { sprites[POS_X].material.opacity = 0.5; sprites[NEG_X].material.opacity = 1; } if (point.y >= 0) { sprites[POS_Y].material.opacity = 1; sprites[NEG_Y].material.opacity = 0.5; } else { sprites[POS_Y].material.opacity = 0.5; sprites[NEG_Y].material.opacity = 1; } if (point.z >= 0) { sprites[POS_Z].material.opacity = 1; sprites[NEG_Z].material.opacity = 0.5; } else { sprites[POS_Z].material.opacity = 0.5; sprites[NEG_Z].material.opacity = 1; } } //# sourceMappingURL=ViewHelper2.js.map