| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- import {
- BackSide,
- Camera,
- CanvasTexture,
- Clock,
- Color,
- Euler,
- LinearFilter,
- Material,
- Mesh,
- MeshBasicMaterial,
- Object3D,
- OrthographicCamera,
- PerspectiveCamera,
- 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
-
- export type GizmoOrientation = '+x' | '-x' | '+y' | '-y' | '+z' | '-z'
-
- export type DomPlacement =
- | 'top-left'
- | 'top-right'
- | 'top-center'
- | 'center-right'
- | 'center-left'
- | 'center-center'
- | 'bottom-left'
- | 'bottom-right'
- | 'bottom-center'
-
- /**
- * 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 {
- camera: OrthographicCamera | PerspectiveCamera
- orthoCamera = new OrthographicCamera(-1.8, 1.8, 1.8, -1.8, 0, 4)
- isViewHelper = true
- @onChangeDispatchEvent()
- animating = false
- target = new Vector3()
- backgroundSphere: Mesh
- axesLines: LineSegments2
- spritePoints: Sprite[]
- domElement: HTMLElement
- domContainer: HTMLElement
- domRect: DOMRect
- // dragging = false
- renderer: WebGLRenderer
- // controls?: OrbitControls | TrackballControls
- // controlsChangeEvent: {listener: () => void}
- viewport: Vector4 = new Vector4()
- offsetHeight = 0
-
- constructor(
- camera: PerspectiveCamera | OrthographicCamera,
- canvas: HTMLCanvasElement,
- placement: DomPlacement = 'bottom-right',
- size = 128,
- pixelRatio = 2,
- ) {
- super()
-
- 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: PointerEvent) {
- // if (this.dragging) return;
- (this.backgroundSphere.material as Material).opacity = 0.4
- this.handleHover(e)
- this.dispatchEvent({type: 'update', event: e})
- }
-
- onPointerLeave(e: PointerEvent) {
- // if (this.dragging) return;
- (this.backgroundSphere.material as Material).opacity = 0.2
- resetSprites(this.spritePoints)
- this.domContainer.style.cursor = ''
- this.dispatchEvent({type: 'update', event: e})
- }
-
- handleClick(e: PointerEvent|MouseEvent) {
- const object = getIntersectionObject(
- e,
- this.domRect,
- this.orthoCamera,
- this.spritePoints
- )
-
- if (!object) return
-
- this.setOrientation(object.userData.type)
- }
-
- handleHover(e: PointerEvent) {
- 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: number) {
- 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: GizmoOrientation) {
- prepareAnimationData(this.camera, this.target, orientation)
- this.animating = true
- this.dispatchEvent({type: 'update'})
- }
-
- dispose() {
- this.axesLines.geometry.dispose();
- (this.axesLines.material as Material).dispose()
-
- this.backgroundSphere.geometry.dispose();
- (this.backgroundSphere.material as 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
- // )
- }
- }
-
- function getDomContainer(placement: DomPlacement, size: number) {
- 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'] as const
- 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: Color, text: 'x' | 'y' | 'z' | null = 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: OrthographicCamera | PerspectiveCamera,
- focusPoint: Vector3,
- axis: GizmoOrientation
- ) {
- 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: Camera, focusPoint: Vector3) {
- radius = camera.position.distanceTo(focusPoint)
- }
-
- function prepareQuaternions(camera: Camera, focusPoint: Vector3) {
- 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: PointerEvent|MouseEvent,
- domRect: DOMRect,
- orthoCamera: OrthographicCamera
- ) {
- 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: PointerEvent|MouseEvent,
- domRect: DOMRect,
- orthoCamera: OrthographicCamera,
- intersectionObjects: Sprite[]
- ) {
- updatePointer(event, domRect, orthoCamera)
-
- const intersects = raycaster.intersectObjects(intersectionObjects)
-
- if (!intersects.length) return null
-
- const intersection = intersects[0]
- return intersection.object as Sprite
- }
-
- function resetSprites(sprites: Sprite[]) {
- 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: Sprite[], camera: 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
- }
- }
|