Skip to content

Commit afa4439

Browse files
authored
Add TrackballControls implementation and sample (#87)
- Implement TrackballControls facade in modules/three/src/main/scala/THREE/controls/TrackballControls.scala - Create TrackballControlsSample demonstrating trackball camera manipulation - Add route for TrackballControlsSample in Router - Add link to TrackballControls in HomePage - Update specs/todo.md to mark TrackballControls as completed
1 parent 1c184f3 commit afa4439

5 files changed

Lines changed: 295 additions & 2 deletions

File tree

example/client/src/main/scala/dev/cheleb/scalajswebgl/app/HomePage.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ object HomePage:
137137
demo("TransformControls", Router.uiRoute("demo", "three", "transformcontrols")),
138138
demo("PointerLockControls", Router.uiRoute("demo", "three", "pointerlockcontrols")),
139139
demo("FlyControls", Router.uiRoute("demo", "three", "flycontrols")),
140-
demo("FirstPersonControls", Router.uiRoute("demo", "three", "firstpersoncontrols"))
140+
demo("FirstPersonControls", Router.uiRoute("demo", "three", "firstpersoncontrols")),
141+
demo("TrackballControls", Router.uiRoute("demo", "three", "trackballcontrols"))
141142
)
142143
),
143144
div(

example/client/src/main/scala/dev/cheleb/scalajswebgl/app/Router.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ object Router:
264264
},
265265
path("firstpersoncontrols") {
266266
dev.cheleb.scalajswebgl.samples.three.FirstPersonControlsSample()
267+
},
268+
path("trackballcontrols") {
269+
dev.cheleb.scalajswebgl.samples.three.TrackballControlsSample()
267270
}
268271
)
269272
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package dev.cheleb.scalajswebgl.samples.three
2+
3+
import com.raquo.laminar.api.L.*
4+
5+
import THREE.*
6+
7+
import org.scalajs.dom
8+
import org.scalajs.dom.window
9+
import scala.scalajs.js
10+
11+
object TrackballControlsSample {
12+
13+
def apply() =
14+
15+
val trackballControlsDiv = div(
16+
h1("TrackballControls Demo"),
17+
p("Demonstrating trackball-style camera manipulation similar to CAD applications."),
18+
div(
19+
cls := "info-panel",
20+
p("TrackballControls provides intuitive camera control without maintaining a constant up vector."),
21+
p("Unlike OrbitControls, the camera can freely rotate over the poles without flipping."),
22+
ul(
23+
li("Left mouse button: Rotate camera around target"),
24+
li("Middle mouse button: Zoom in/out"),
25+
li("Right mouse button: Pan camera"),
26+
li("Mouse wheel: Zoom in/out"),
27+
li("A/S/D keys: Rotate, zoom, pan respectively (modifier keys)")
28+
)
29+
),
30+
div(
31+
cls := "canvas-container"
32+
)
33+
)
34+
35+
// Create scene
36+
val scene = Scene()
37+
38+
// Create camera
39+
val camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
40+
camera.position.set(0, 0, 10)
41+
42+
// Create WebGL renderer
43+
val renderer = WebGLRenderer(antialias = true)
44+
renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.8)
45+
renderer.setClearColor("#0a0a0a", 1)
46+
47+
// Create a scene with multiple objects arranged in a sphere
48+
val geometries = js.Array(
49+
new SphereGeometry(1.0, 16, 12),
50+
new BoxGeometry(2.0, 2.0, 2.0),
51+
new ConeGeometry(1.0, 3.0, 8),
52+
new CylinderGeometry(0.8, 0.8, 2.0, 16),
53+
new TorusGeometry(1.5, 0.4, 8, 16),
54+
new OctahedronGeometry(1.5, 0),
55+
new IcosahedronGeometry(1.5, 0),
56+
new DodecahedronGeometry(1.5, 0)
57+
)
58+
59+
// Create materials with different colors
60+
val materials = js.Array(
61+
MeshStandardMaterial(color = 0xff4444, roughness = 0.3, metalness = 0.7),
62+
MeshStandardMaterial(color = 0x44ff44, roughness = 0.5, metalness = 0.3),
63+
MeshStandardMaterial(color = 0x4444ff, roughness = 0.2, metalness = 0.8),
64+
MeshStandardMaterial(color = 0xffff44, roughness = 0.4, metalness = 0.5),
65+
MeshStandardMaterial(color = 0xff44ff, roughness = 0.6, metalness = 0.2),
66+
MeshStandardMaterial(color = 0x44ffff, roughness = 0.1, metalness = 0.9),
67+
MeshStandardMaterial(color = 0xff8844, roughness = 0.7, metalness = 0.1),
68+
MeshStandardMaterial(color = 0x8844ff, roughness = 0.8, metalness = 0.4)
69+
)
70+
71+
// Create meshes and position them in a spherical arrangement
72+
val meshes = geometries.zipWithIndex.map { case (geometry, i) =>
73+
val mesh = new Mesh(geometry, materials(i))
74+
75+
// Arrange objects in a sphere around the origin
76+
val phi = scala.math.Pi * 2 * i / geometries.length
77+
val theta = scala.math.Pi * (i % 2) * 0.5
78+
val radius = 8.0
79+
80+
mesh.position.set(
81+
radius * scala.math.sin(theta) * scala.math.cos(phi),
82+
radius * scala.math.cos(theta),
83+
radius * scala.math.sin(theta) * scala.math.sin(phi)
84+
)
85+
86+
scene.add(mesh)
87+
mesh
88+
}
89+
90+
// Add a central object as the target
91+
val centerGeometry = new SphereGeometry(0.5, 16, 12)
92+
val centerMaterial = MeshStandardMaterial(color = 0xffffff, roughness = 0.1, metalness = 0.9)
93+
val centerMesh = new Mesh(centerGeometry, centerMaterial)
94+
scene.add(centerMesh)
95+
96+
// Add lighting
97+
val directionalLight = DirectionalLight(0xffffff, 1)
98+
directionalLight.position.set(10, 10, 5)
99+
scene.add(directionalLight)
100+
101+
val ambientLight = AmbientLight(0x404040, 0.3)
102+
scene.add(ambientLight)
103+
104+
// Add some point lights for atmosphere
105+
val pointLight1 = PointLight(0xff0000, 0.5, 30)
106+
pointLight1.position.set(-10, 5, 0)
107+
scene.add(pointLight1)
108+
109+
val pointLight2 = PointLight(0x0000ff, 0.5, 30)
110+
pointLight2.position.set(10, 5, 0)
111+
scene.add(pointLight2)
112+
113+
val pointLight3 = PointLight(0x00ff00, 0.5, 30)
114+
pointLight3.position.set(0, 0, 10)
115+
scene.add(pointLight3)
116+
117+
// Create TrackballControls
118+
val controls = TrackballControls(camera, renderer.domElement)
119+
controls.rotateSpeed = 1.0
120+
controls.zoomSpeed = 1.2
121+
controls.panSpeed = 0.8
122+
controls.noRotate = false
123+
controls.noZoom = false
124+
controls.noPan = false
125+
controls.staticMoving = false
126+
controls.dynamicDampingFactor = 0.3
127+
controls.minDistance = 1.0
128+
controls.maxDistance = 100.0
129+
130+
// Set target to center
131+
controls.target.set(0, 0, 0)
132+
133+
// Animation loop
134+
val animate: () => Unit = () => {
135+
val time = js.Date.now() * 0.001
136+
137+
// Animate objects for visual interest
138+
meshes.zipWithIndex.foreach { case (mesh, i) =>
139+
mesh.rotation.x = time * (0.5 + i * 0.1)
140+
mesh.rotation.y = time * (0.3 + i * 0.15)
141+
}
142+
143+
// Rotate center object
144+
centerMesh.rotation.x = time * 0.5
145+
centerMesh.rotation.y = time * 0.8
146+
147+
// Update controls
148+
controls.update()
149+
150+
// Render the scene
151+
renderer.clear()
152+
renderer.render(scene, camera)
153+
}
154+
renderer.setAnimationLoop(animate)
155+
156+
// Handle window resize
157+
val onWindowResize: dom.Event => Unit = { _ =>
158+
camera.aspect = window.innerWidth / window.innerHeight
159+
camera.updateProjectionMatrix()
160+
renderer.setSize(window.innerWidth * 0.8, window.innerHeight * 0.8)
161+
controls.handleResize()
162+
}
163+
window.addEventListener("resize", onWindowResize)
164+
165+
// Cleanup on unmount
166+
trackballControlsDiv.amend(
167+
onUnmountCallback { _ =>
168+
controls.dispose()
169+
renderer.dispose()
170+
window.removeEventListener("resize", onWindowResize)
171+
}
172+
)
173+
174+
// Append renderer to the canvas container
175+
trackballControlsDiv.ref.querySelector(".canvas-container").appendChild(renderer.domElement)
176+
177+
trackballControlsDiv
178+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package THREE
2+
3+
import scala.scalajs.js
4+
import scala.scalajs.js.annotation.*
5+
import org.scalajs.dom
6+
7+
/**
8+
* This class is similar to {@link OrbitControls}. However, it does not maintain
9+
* a constant camera `up` vector. That means if the camera orbits over the
10+
* "north" and "south" poles, it does not flip to stay "right side up".
11+
*/
12+
@js.native
13+
@JSImport("three/addons/controls/TrackballControls.js", "TrackballControls")
14+
class TrackballControls(camera: Camera, domElement: js.UndefOr[dom.Element] = js.undefined) extends js.Object {
15+
16+
/**
17+
* The rotation speed.
18+
*/
19+
var rotateSpeed: Double = js.native
20+
21+
/**
22+
* The zoom speed.
23+
*/
24+
var zoomSpeed: Double = js.native
25+
26+
/**
27+
* The pan speed.
28+
*/
29+
var panSpeed: Double = js.native
30+
31+
/**
32+
* Whether rotation is disabled or not.
33+
*/
34+
var noRotate: Boolean = js.native
35+
36+
/**
37+
* Whether zooming is disabled or not.
38+
*/
39+
var noZoom: Boolean = js.native
40+
41+
/**
42+
* Whether panning is disabled or not.
43+
*/
44+
var noPan: Boolean = js.native
45+
46+
/**
47+
* Whether damping is disabled or not.
48+
*/
49+
var staticMoving: Boolean = js.native
50+
51+
/**
52+
* Defines the intensity of damping. Only considered if `staticMoving` is set
53+
* to `false`.
54+
*/
55+
var dynamicDampingFactor: Double = js.native
56+
57+
/**
58+
* How far you can dolly in (perspective camera only).
59+
*/
60+
var minDistance: Double = js.native
61+
62+
/**
63+
* How far you can dolly out (perspective camera only).
64+
*/
65+
var maxDistance: Double = js.native
66+
67+
/**
68+
* How far you can zoom in (orthographic camera only).
69+
*/
70+
var minZoom: Double = js.native
71+
72+
/**
73+
* How far you can zoom out (orthographic camera only).
74+
*/
75+
var maxZoom: Double = js.native
76+
77+
/**
78+
* This array holds keycodes for controlling interactions.
79+
*/
80+
var keys: js.Array[String] = js.native
81+
82+
/**
83+
* This object contains references to the mouse actions used by the controls.
84+
*/
85+
var mouseButtons: js.Object = js.native
86+
87+
/**
88+
* The focus point of the controls.
89+
*/
90+
var target: Vector3 = js.native
91+
92+
/**
93+
* Update the controls. Call this method every frame.
94+
*/
95+
def update(): Unit = js.native
96+
97+
/**
98+
* Resets the controls to its initial state.
99+
*/
100+
def reset(): Unit = js.native
101+
102+
/**
103+
* Must be called if the application window is resized.
104+
*/
105+
def handleResize(): Unit = js.native
106+
107+
/**
108+
* Dispose of the controls.
109+
*/
110+
def dispose(): Unit = js.native
111+
}

specs/todo.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Based on the current implementation, here's a comprehensive list of Three.js com
131131
- [x] **FlyControls** - Flight simulator controls
132132
- [-] **DeviceOrientationControls** - Mobile device orientation
133133
- [x] **FirstPersonControls** - First-person camera controls
134-
- **TrackballControls** - Trackball camera manipulation
134+
- [x] **TrackballControls** - Trackball camera manipulation
135135

136136
### **Curves and Paths** (Completely Missing)
137137
- **Curve** - Base curve class

0 commit comments

Comments
 (0)