forked from realitymediabook/core-components
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththree-sample.js
More file actions
401 lines (346 loc) · 16.2 KB
/
three-sample.js
File metadata and controls
401 lines (346 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
/**
* Description
* ===========
* create a threejs object (two cubes, one on the surface of the other) that can be interacted
* with and has some networked attributes.
*
*/
import {
interactiveComponentTemplate,
registerSharedAFRAMEComponents
} from "../utils/interaction";
///////////////////////////////////////////////////////////////////////////////
// simple convenience functions
function randomColor() {
return new THREE.Color(Math.random(), Math.random(), Math.random());
}
function almostEqualVec3(u, v, epsilon) {
return Math.abs(u.x - v.x) < epsilon && Math.abs(u.y - v.y) < epsilon && Math.abs(u.z - v.z) < epsilon;
};
function almostEqualColor(u, v, epsilon) {
return Math.abs(u.r - v.r) < epsilon && Math.abs(u.g - v.g) < epsilon && Math.abs(u.b - v.b) < epsilon;
};
// a lot of the complexity has been pulled out into methods in the object
// created by interactiveComponentTemplate() and registerSharedAFRAMEcomponents().
// Here, we define methods that are used by the object there, to do our object-specific
// work.
// We need to define:
// - AFRAME
// - schema
// - init() method, which should can startInit() and finishInit()
// - update() and play() if you need them
// - tick() and tick2() to handle frame updates
//
// - change isNetworked, isInteractive, isDraggable (default: false) to reflect what
// the object needs to do.
// - loadData() is an async function that does any slow work (loading things, etc)
// and is called by finishInit(), which waits till it's done before setting things up
// - initializeData() is called to set up the initial state of the object, a good
// place to create the 3D content. The three.js scene should be added to
// this.simpleContainter
// - clicked() is called when the object is clicked
// - dragStart() is called right after clicked() if isDraggable is true, to set up
// for a possible drag operation
// - dragEnd() is called when the mouse is released
// - drag() should be called each frame while the object is being dragged (between
// dragStart() and dragEnd())
// - getInteractors() returns an array of objects for which interaction controls are
// intersecting the object. There will likely be zero, one, or two of these (if
// there are two controllers and both are pointing at the object). The "cursor"
// field is a pointer to the small sphere Object3D that is displayed where the
// interaction ray touches the object. The "controller" field is the
/// corresponding controller
// object that includes things like the rayCaster.
// - getIntersection() takes in the interactor and the three.js object3D array
// that should be tested for interaction.
// Note that only the entity that this component is attached to will be "seen"
// by Hubs interaction system, so the entire three.js tree below it triggers
// click and drag events. The getIntersection() method is needed
// the componentName must be lowercase, can have hyphens, start with a letter,
// but no underscores
let componentName = "test-cube";
// get the template part of the object need for the AFRAME component
let template = interactiveComponentTemplate(componentName);
// create the additional parts of the object needed for the AFRAME component
let child = {
schema: {
// name is hopefully unique for each instance
name: {
type: "string",
default: ""
},
// the template will look for these properties. If they aren't there, then
// the lookup (this.data.*) will evaluate to falsey
isNetworked: {
type: "boolean",
default: false
},
isInteractive: {
type: "boolean",
default: true
},
isDraggable: {
type: "boolean",
default: true
},
// our data
width: {
type: "number",
default: 1
},
color: {
type: "string",
default: ""
},
parameter1: {
type: "string",
default: ""
}
},
// fullName is used to generate names for the AFRame objects we create. Should be
// unique for each instance of an object, which we specify with name. If name does
// name get used as a scheme parameter, it defaults to the name of it's parent glTF
// object, which only works if those are uniquely named.
init: function () {
this.startInit();
// the template uses these to set things up. relativeSize
// is used to set the size of the object relative to the size of the image
// that it's attached to: a size of 1 means
// "the size of 1x1x1 units in the object
// space will be the same as the size of the image".
// Larger relative sizes will make the object smaller because we are
// saying that a size of NxNxN maps to the Size of the image, and vice versa.
// For example, if the object below is 2,2 in size and we set size 2, then
// the object will remain the same size as the image. If we leave it at 1,1,
// then the object will be twice the size of the image.
this.relativeSize = this.data.width;
// override the defaults in the template
this.isDraggable = this.data.isDraggable;
this.isInteractive = this.data.isInteractive;
this.isNetworked = this.data.isNetworked;
// our potentiall-shared object state (two roations and two colors for the boxes)
this.sharedData = {
color: new THREE.Color(this.data.color.length > 0 ? this.data.color : "grey"),
rotation: new THREE.Euler(),
position: new THREE.Vector3(0,0.5,0)
};
// some local state
this.initialEuler = new THREE.Euler()
// some click/drag state
this.clickEvent = null
this.clickIntersection = null
// we should set fullName if we have a meaningful name
if (this.data.name && this.data.name.length > 0) {
this.fullName = this.data.name;
}
// finish the initialization
this.finishInit();
},
// if anything changed in this.data, we need to update the object.
// this is probably not going to happen, but could if another of
// our scripts modifies the component properties in the DOM
update: function () {},
// do some stuff to get async data. Called by initTemplate()
loadData: async function () {
return
},
// called by initTemplate() when the component is being processed. Here, we create
// the three.js objects we want, and add them to simpleContainer (an AFrame node
// the template created for us).
initializeData: function () {
this.box = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1, 2, 2, 2),
new THREE.MeshBasicMaterial({
color: this.sharedData.color
})
);
this.box.matrixAutoUpdate = true;
this.simpleContainer.setObject3D('box', this.box)
// create a second small, black box on the surface of the box
this.box2 = new THREE.Mesh(
new THREE.BoxGeometry(0.1, 0.1, 0.1, 2, 2, 2),
new THREE.MeshBasicMaterial({
color: "black"
})
);
this.box2.matrixAutoUpdate = true;
this.box2.position.copy(this.sharedData.position)
// add it as a child of the first box, since we want it to move with the first box
this.box.add(this.box2)
// IMPORTANT: any three.js object that is added to a Hubs (aframe) entity
// must have ".el" pointing to the AFRAME Entity that contains it.
// When an object3D is added with ".setObject3D", it is added to the
// object3D for that Entity, and sets all of the children of that
// object3D to point to the same Entity. If you add an object3D to
// the sub-tree of that object later, you must do this yourself.
this.box2.el = this.simpleContainer
// tell the portals to update their view
this.el.sceneEl.emit('updatePortals')
},
// called from remove() in the template to remove any local resources when the component
// is destroyed
remove: function () {
this.simpleContainer.removeObject3D("box")
this.box.geometry.dispose()
this.box.material.dispose()
this.box2.geometry.dispose()
this.box2.material.dispose()
this.removeTemplate()
},
// handle "interact" events for clickable entities
clicked: function (evt) {
// the evt.target will point at the object3D in this entity. We can use
// handleInteraction.getInteractionTarget() to get the more precise
// hit information about which object3Ds in our object were hit. We store
// the one that was clicked here, so we know which it was as we drag around
this.clickIntersection = this.handleInteraction.getIntersection(evt.object3D, [evt.target]);
this.clickEvent = evt;
if (!this.clickIntersection) {
console.warn("click didn't hit anything; shouldn't happen");
return;
}
if (this.clickIntersection.object == this.box) {
// new random color on each click
let newColor = randomColor()
this.box.material.color.set(newColor)
this.sharedData.color.set(newColor)
this.setSharedData()
} else if (this.clickIntersection.object == this.box2) {}
},
// called to start the drag. Will be called after clicked() if isDraggable is true
dragStart: function (evt) {
// set up the drag state
if (!this.handleInteraction.startDrag(evt)) {
return
}
// grab a copy of the current orientation of the object we clicked
if (this.clickIntersection.object == this.box) {
this.initialEuler.copy(this.box.rotation)
} else if (this.clickIntersection.object == this.box2) {
this.box2.material.color.set("red")
}
},
// called when the button is released to finish the drag
dragEnd: function (evt) {
this.handleInteraction.endDrag(evt)
if (this.clickIntersection.object == this.box) {} else if (this.clickIntersection.object == this.box2) {
this.box2.material.color.set("black")
}
},
// the method setSharedData() always sets the shared data, causing a network update.
// We can be smarter here by calling it only when significant changes happen,
// which we'll do in the setSharedEuler methods
setSharedEuler: function (newEuler) {
if (!almostEqualVec3(this.sharedData.rotation, newEuler, 0.05)) {
this.sharedData.rotation.copy(newEuler)
this.setSharedData()
}
},
setSharedPosition: function (newPos) {
if (!almostEqualVec3(this.sharedData.position, newPos, 0.05)) {
this.sharedData.position.copy(newPos)
this.setSharedData()
}
},
// if the object is networked, this.stateSync will exist and should be called
setSharedData: function () {
if (this.stateSync) {
return this.stateSync.setSharedData(this.sharedData)
}
return true
},
// this is called from the networked data entity to get the initial data
// from the component
getSharedData: function () {
return this.sharedData
},
// per frame stuff
tick: function (time) {
if (!this.box) {
// haven't finished initializing yet
return;
}
// if it's interactive, we'll handle drag and hover events
if (this.isInteractive) {
// if we're dragging, update the rotation
if (this.isDraggable && this.handleInteraction.isDragging) {
// do something with the dragging. Here, we'll use delta.x and delta.y
// to rotate the object. These values are set as a relative offset in
// the plane perpendicular to the view, so we'll use them to offset the
// x and y rotation of the object. This is a TERRIBLE way to do rotate,
// but it's a simple example.
if (this.clickIntersection.object == this.box) {
// update drag state
this.handleInteraction.drag()
// compute a new rotation based on the delta
this.box.rotation.set(this.initialEuler.x - this.handleInteraction.delta.x,
this.initialEuler.y + this.handleInteraction.delta.y,
this.initialEuler.z)
// update the shared rotation
this.setSharedEuler(this.box.rotation)
} else if (this.clickIntersection.object == this.box2) {
// we want to hit test on our boxes, but only want to know if/where
// we hit the big box. So first hide the small box, and then do a
// a hit test, which can only result in a hit on the big box.
this.box2.visible = false
let intersect = this.handleInteraction.getIntersection(this.handleInteraction.dragInteractor, [this.box])
this.box2.visible = true
// if we hit the big box, move the small box to the position of the hit
if (intersect) {
// the intersect object is a THREE.Intersection object, which has the hit point
// specified in world coordinates. So we move those coordinates into the local
// coordiates of the big box, and then set the position of the small box to that
let position = this.box.worldToLocal(intersect.point)
this.box2.position.copy(position)
this.setSharedPosition(this.box2.position)
}
}
} else {
// do something with the rays when not dragging or clicking.
// For example, we could display some additional content when hovering
let passthruInteractor = this.handleInteraction.getInteractors(this.simpleContainer);
// we will set yellow if either interactor hits the box. We'll keep track of if
// one does
let setIt = false;
// for each of our interactors, check if it hits the scene
for (let i = 0; i < passthruInteractor.length; i++) {
let intersection = this.handleInteraction.getIntersection(passthruInteractor[i], this.simpleContainer.object3D.children)
// if we hit the small box, set the color to yellow, and flag that we hit
if (intersection && intersection.object === this.box2) {
this.box2.material.color.set("yellow")
setIt = true
}
}
// if we didn't hit, make sure the color remains black
if (!setIt) {
this.box2.material.color.set("black")
}
}
}
if (this.isNetworked) {
// if we haven't finished setting up the networked entity don't do anything.
if (!this.netEntity || !this.stateSync) {
return
}
// if the state has changed in the networked data, update our html object
if (this.stateSync.changed) {
this.stateSync.changed = false
// got the data, now do something with it
let newData = this.stateSync.dataObject
this.sharedData.color.set(newData.color)
this.sharedData.rotation.copy(newData.rotation)
this.sharedData.position.copy(newData.position)
this.box.material.color.set(newData.color)
this.box.rotation.copy(newData.rotation)
this.box2.position.copy(newData.position)
}
}
}
}
// register the component with the AFrame scene
AFRAME.registerComponent(componentName, {
...child,
...template
})
// create and register the data component and it's NAF component with the AFrame scene
registerSharedAFRAMEComponents(componentName)