Skip to content

Commit

Permalink
feat: move logic to rotate camera after clicked viewcube to one seper…
Browse files Browse the repository at this point in the history
…ated class SimpleCameraControls (#11)
  • Loading branch information
mlight-lee authored Aug 22, 2024
1 parent 8d65781 commit 2115dd4
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 206 deletions.
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ npm install @mlightcad/three-viewcube
Use it with your `camera` and `renderer` instances.

```javascript
import { ViewCubeHelper } from '@mlightcad/three-viewcube'
import { ViewCubeGizmo } from '@mlightcad/three-viewcube'

// Create your renderer and set alhpa to true
const renderer = new THREE.WebGLRenderer({ alpha: true })
Expand All @@ -35,22 +35,31 @@ const camera = ...
// Create your orbit controller
const cameraControls = new OrbitControls(camera, renderer.domElement)

// Create viewcube helper
const viewCubeHelper = new ViewCubeHelper(camera, renderer)
viewCubeHelper.setControls(cameraControls)
// Create viewcube gizmo
const viewCubeGizmo = new ViewCubeGizmo(camera, renderer)

// Animation loop
function animate() {
requestAnimationFrame(animate)
renderer.clear()
renderer.render(scene, camera)
viewCubeHelper.render(renderer)
viewCubeGizmo.update()
}

animate()
```

You can customize view cube by passing one `ViewCubeOptions` instance when creating one ViewCubeHelper instance. Defintion of `ViewCubeOptions` is as follows.
If you want to rotate the current view after clicked face, edge, or corner of viewcube. You need to listen to 'change' event of `ViewCubeGizmo`.

```javascript
viewCubeGizmo.addEventListener('change', event => {
// TODO: Add you own logic to rotate the view
})
```

To correctly rotate current view, you need to consider bounding box of objects and camera (position, lookAt, movement, rotation) in current view and camera. Class `SimpleCameraControls` is provided to faciliate it. However, `SimpleCameraControls` just considers camera roation. You can refine `SimpleCameraControls` by yourselves.

You can customize view cube by passing one `ViewCubeOptions` instance when creating one `ViewCubeGizmo` instance. Defintion of `ViewCubeOptions` is as follows.

```javascript
/**
Expand Down Expand Up @@ -89,7 +98,7 @@ export interface ViewCubeOptions {
For example, you can set view cube options as follows if you want to set text shown in each face to Chinese.

```javascript
import { FaceNames, ViewCubeHelper } from '@mlightcad/three-viewcube'
import { FaceNames, ViewCubeGizmo } from '@mlightcad/three-viewcube'

// Create you camera and render
......
Expand All @@ -102,7 +111,7 @@ const faceNames: FaceNames = {
left: '',
bottom: ''
}
const viewCubeHelper = new ViewCubeHelper(camera, renderer, { faceNames: faceNames })
const viewCubeGizmo = new ViewCubeGizmo(camera, renderer, { faceNames: faceNames })

```

Expand Down
1 change: 1 addition & 0 deletions packages/three-viewcube-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
},
"dependencies": {
"@mlightcad/three-viewcube": "^0.0.5",
"@tweenjs/tween.js": "^25.0.0",
"three": "^0.166.1"
}
}
44 changes: 28 additions & 16 deletions packages/three-viewcube-demo/src/viewer.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import {
Axes2dHelper,
AxesGizmo,
FaceNames,
SimpleCameraControls,
ViewCube,
ViewCubeHelper
ViewCubeGizmo
} from '@mlightcad/three-viewcube'

export class Viewer {
private _scene: THREE.Scene
private _camera: THREE.PerspectiveCamera
private _renderer: THREE.WebGLRenderer
private _cameraControls: OrbitControls
private _viewCubeHelper: ViewCubeHelper
private _axes2dHelper: Axes2dHelper
private _viewCubeGizmo: ViewCubeGizmo
private _axesGizmo: AxesGizmo
private _simpleCameraControls: SimpleCameraControls
private _bbox: THREE.Box3

constructor() {
Expand All @@ -22,8 +24,9 @@ export class Viewer {
this._camera = this.createCamera()
this._renderer = this.creatRenderer()
this._cameraControls = this.createCameraControls()
this._viewCubeHelper = this.createViewCubeHelper()
this._axes2dHelper = this.createAxes2dHelper()
this._simpleCameraControls = this.createSimpleCameraControls()
this._viewCubeGizmo = this.createViewCubeGizmo()
this._axesGizmo = this.createAxesGizmo()
this.createLights()
this.createObjects()
}
Expand Down Expand Up @@ -52,8 +55,9 @@ export class Viewer {
requestAnimationFrame(this.animate.bind(this))
this.renderer.clear()
this.renderer.render(this.scene, this.camera)
this._viewCubeHelper.render()
this._axes2dHelper.render()
this._viewCubeGizmo.update()
this._axesGizmo.update()
this._simpleCameraControls.update()
}

private createScene() {
Expand Down Expand Up @@ -109,6 +113,12 @@ export class Viewer {
return cameraControls
}

private createSimpleCameraControls() {
const simpleCameraControls = new SimpleCameraControls(this.camera)
simpleCameraControls.setControls(this.cameraControls)
return simpleCameraControls
}

private createObjects() {
const planeGeometry = new THREE.PlaneGeometry(3, 3)
const planeMaterial = new THREE.MeshLambertMaterial({
Expand Down Expand Up @@ -140,15 +150,17 @@ export class Viewer {
if (axes.geometry.boundingBox) this._bbox.union(axes.geometry.boundingBox)
}

private createViewCubeHelper() {
const viewCubeHelper = new ViewCubeHelper(this.camera, this.renderer)
viewCubeHelper.setControls(this.cameraControls)
return viewCubeHelper
private createViewCubeGizmo() {
const viewCubeGizmo = new ViewCubeGizmo(this.camera, this.renderer)
viewCubeGizmo.addEventListener('change', event => {
this._simpleCameraControls.flyTo(event.quaternion)
})
return viewCubeGizmo
}

private createAxes2dHelper() {
const axes2dHelper = new Axes2dHelper(this.camera, this.renderer)
axes2dHelper.setTextColor(new THREE.Color(0x00ff00))
return axes2dHelper
private createAxesGizmo() {
const axes2dGizmo = new AxesGizmo(this.camera, this.renderer)
axes2dGizmo.setTextColor(new THREE.Color(0x00ff00))
return axes2dGizmo
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import * as THREE from 'three'
import { createTextSprite } from './viewCubeData'
import { FixedPosObject, ObjectPosition } from './fixedPosObject'
import { FixedPosGizmo, ObjectPosition } from './fixedPosGizmo'

/**
* An axis object to visualize the 2 axes in a simple way.
* The X axis is red and the Y axis is green by default
* An axis gizmo to visualize the axes in a simple way.
* The X axis is red, the Y axis is green, and the Z axis is blue by default
*/
export class Axes2dHelper extends FixedPosObject {
export class AxesGizmo extends FixedPosGizmo {
private axes: THREE.LineSegments
private xText: THREE.Sprite
private yText: THREE.Sprite
private zText?: THREE.Sprite
private hasZAxis: boolean

constructor(
camera: THREE.PerspectiveCamera | THREE.OrthographicCamera,
renderer: THREE.WebGLRenderer,
size = 100
size = 100,
hasZAxis = true
) {
super(camera, renderer, size, ObjectPosition.LEFT_BOTTOM)
const vertices = [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0]
this.hasZAxis = hasZAxis

const vertices = [0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 2, 0]
const colors = [1, 0, 0, 1, 0.6, 0, 0, 1, 0, 0.6, 1, 0]
if (hasZAxis) {
vertices.push(0, 0, 0, 0, 0, 2)
colors.push(0, 0, 1, 0, 0.6, 1)
}

const geometry = new THREE.BufferGeometry()
geometry.setAttribute(
Expand All @@ -34,16 +42,22 @@ export class Axes2dHelper extends FixedPosObject {
})

this.axes = new THREE.LineSegments(geometry, material)
this.axes.position.set(-1, -1, 0)
this.axes.position.set(-1, -1, -1)
this.add(this.axes)

this.xText = createTextSprite('X')
this.xText.position.set(1.5, -1, 0)
this.xText.position.set(1.5, -1, -1)
this.add(this.xText)

this.yText = createTextSprite('Y')
this.yText.position.set(-1, 1.5, 0)
this.yText.position.set(-1, 1.5, -1)
this.add(this.yText)

if (hasZAxis) {
this.zText = createTextSprite('Z')
this.zText.position.set(-1, -1, 1.5)
this.add(this.zText)
}
}

/**
Expand Down Expand Up @@ -91,5 +105,10 @@ export class Axes2dHelper extends FixedPosObject {

this.yText.geometry.dispose()
this.yText.material.dispose()

if (this.hasZAxis) {
this.zText?.geometry.dispose()
this.zText?.material.dispose()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as THREE from 'three'

/**
* Enum to define postion of the object.
* Enum to define postion of the gizmo.
*/
export enum ObjectPosition {
LEFT_BOTTOM = 0,
Expand All @@ -10,26 +10,23 @@ export enum ObjectPosition {
RIGHT_BOTTOM = 4
}

const clock = new THREE.Clock()

/**
* A customizable object with fixed postion in viewport
* A customizable gizmo with fixed postion in viewport
*/
export class FixedPosObject extends THREE.Object3D {
protected objectCamera: THREE.OrthographicCamera
export class FixedPosGizmo<TEventMap extends THREE.Object3DEventMap = THREE.Object3DEventMap> extends THREE.Object3D<TEventMap> {
protected gizmoCamera: THREE.OrthographicCamera
protected renderer: THREE.WebGLRenderer
protected camera: THREE.PerspectiveCamera | THREE.OrthographicCamera
protected objectDim: number
protected objectPos: ObjectPosition
protected animating: boolean
protected gizmoDim: number
protected gizmoPos: ObjectPosition

/**
* Construct one instance of this class
* Construct one instance of this gizmo
* @param camera Camera used in your canvas
* @param renderer Renderer used in your canvas
* @param dimension Size of area ocupied by this object. Because width and height of this area is same,
* @param dimension Size of area ocupied by this gizmo. Because width and height of this area is same,
* it is single value. The real size of the objet will be calculated automatically considering rotation.
* @param pos Position of the object
* @param pos Position of the gizmo
*/
constructor(
camera: THREE.PerspectiveCamera | THREE.OrthographicCamera,
Expand All @@ -42,27 +39,25 @@ export class FixedPosObject extends THREE.Object3D {
this.camera = camera
this.renderer = renderer

this.objectCamera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0, 4)
this.objectCamera.position.set(0, 0, 2)
this.gizmoCamera = new THREE.OrthographicCamera(-2, 2, 2, -2, 0, 4)
this.gizmoCamera.position.set(0, 0, 2)

this.gizmoDim = dimension
this.gizmoPos = pos

this.objectDim = dimension
this.objectPos = pos
this.animating = false
this.initialize()
}

/**
* Function called by constructor to initialize the object. The children class can override this function
* Function called by constructor to initialize this gizmo. The children class can override this function
* to add its own initialization logic.
*/
initialize() {}

/**
* Render this object
* Update and rerender this gizmo
*/
render() {
const delta = clock.getDelta()
if (this.animating) this.animate(delta)

update() {
this.updateOrientation()

// Store autoClear flag value
Expand All @@ -73,23 +68,14 @@ export class FixedPosObject extends THREE.Object3D {
const viewport = new THREE.Vector4()
this.renderer.getViewport(viewport)
const pos = this.calculateViewportPos()
this.renderer.setViewport(pos.x, pos.y, this.objectDim, this.objectDim)
this.renderer.render(this, this.objectCamera)
this.renderer.setViewport(pos.x, pos.y, this.gizmoDim, this.gizmoDim)
this.renderer.render(this, this.gizmoCamera)
this.renderer.setViewport(viewport.x, viewport.y, viewport.z, viewport.w)

// Restore autoClear flag vlaue
this.renderer.autoClear = autoClear
}

/**
* Animation loop
* @param delta The seconds passed since the time clock's oldTime was set and sets clock's oldTime to the
* current time.
*/
protected animate(_delta: number) {
// Do nothing for now. The children class can override this function to add its own logic.
}

/**
* Free the GPU-related resources allocated by this instance. Call this method whenever this instance
* is no longer used in your app.
Expand All @@ -106,17 +92,17 @@ export class FixedPosObject extends THREE.Object3D {
offsetY: number,
bbox: THREE.Box2
) {
const x = ((offsetX - bbox.min.x) / this.objectDim) * 2 - 1
const y = -((offsetY - bbox.min.y) / this.objectDim) * 2 + 1
const x = ((offsetX - bbox.min.x) / this.gizmoDim) * 2 - 1
const y = -((offsetY - bbox.min.y) / this.gizmoDim) * 2 + 1
return { x, y }
}

protected calculateViewportPos() {
const domElement = this.renderer.domElement
const canvasWidth = domElement.offsetWidth
const canvasHeight = domElement.offsetHeight
const pos = this.objectPos
const length = this.objectDim
const pos = this.gizmoPos
const length = this.gizmoDim
let x = canvasWidth - length
let y = canvasHeight - length
switch (pos) {
Expand All @@ -138,8 +124,8 @@ export class FixedPosObject extends THREE.Object3D {
const domElement = this.renderer.domElement
const canvasWidth = domElement.offsetWidth
const canvasHeight = domElement.offsetHeight
const pos = this.objectPos
const length = this.objectDim
const pos = this.gizmoPos
const length = this.gizmoDim
const bbox = new THREE.Box2(
new THREE.Vector2(canvasWidth - length, 0),
new THREE.Vector2(canvasWidth, length)
Expand Down
7 changes: 4 additions & 3 deletions packages/three-viewcube/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './faceNames'
export * from './fixedPosObject'
export * from './fixedPosGizmo'
export * from './viewCube'
export * from './viewCubeHelper'
export * from './axes2dHelper'
export * from './viewCubeGizmo'
export * from './axesGizmo'
export * from './simpleCameraControls'
Loading

0 comments on commit 2115dd4

Please sign in to comment.