Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39551
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
###############################################################################
13
14
$ = window.$
15
async = require('async')
16
17
misc = require('smc-util/misc')
18
{defaults, required} = misc
19
20
component_to_hex = (c) ->
21
hex = c.toString(16)
22
if hex.length == 1
23
return "0" + hex
24
else
25
return hex
26
27
rgb_to_hex = (r, g, b) -> "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b)
28
29
_loading_threejs_callbacks = []
30
31
VERSION = '73'
32
33
window.THREE = require("three")
34
#for m in ['OrbitControls', 'CanvasRenderer', 'Projector']
35
# require("script!threejs/r#{VERSION}/#{m}")
36
37
require("script!./node_modules/three/examples/js/controls/OrbitControls")
38
require("script!./node_modules/three/examples/js/renderers/CanvasRenderer")
39
require("script!./node_modules/three/examples/js/renderers/Projector")
40
41
#require("script!threejs/r#{VERSION}/Detector")
42
require("script!./node_modules/three/examples/js/Detector")
43
44
_scene_using_renderer = undefined
45
_renderer = {webgl:undefined, canvas:undefined}
46
dynamic_renderer_type = undefined
47
48
get_renderer = (scene, type) ->
49
# if there is a scene currently using this renderer, tell it to switch to
50
# the static renderer.
51
if _scene_using_renderer? and _scene_using_renderer._id != scene._id
52
_scene_using_renderer.set_static_renderer()
53
54
# now scene takes over using this renderer
55
_scene_using_renderer = scene
56
if Detector.webgl and (not type? or type == 'webgl')
57
type = 'webgl'
58
else
59
type = 'canvas'
60
dynamic_renderer_type = type
61
if not _renderer[type]?
62
# get the best-possible THREE.js renderer (once and for all)
63
if type == 'webgl'
64
_renderer[type] = new THREE.WebGLRenderer
65
antialias : true
66
alpha : true
67
preserveDrawingBuffer : true
68
else
69
_renderer[type] = new THREE.CanvasRenderer
70
antialias : true
71
alpha : true
72
$(_renderer[type].domElement).addClass("webapp-3d-dynamic-renderer")
73
return _renderer[type]
74
75
MIN_WIDTH = MIN_HEIGHT = 16
76
77
class WebappThreeJS
78
constructor: (opts) ->
79
@opts = defaults opts,
80
element : required
81
container : required
82
width : undefined
83
height : undefined
84
renderer : undefined # 'webgl' or 'canvas' or undefined to choose best
85
background : "transparent"
86
foreground : undefined
87
spin : false # if true, image spins by itself when mouse is over it.
88
camera_distance : 10
89
aspect_ratio : undefined # undefined does nothing or a triple [x,y,z] of length three,
90
# which scales the x,y,z coordinates of everything by the given values.
91
stop_when_gone : undefined # if given, animation, etc., stops when this html element (not jquery!) is no longer in the DOM
92
frame : undefined # if given call set_frame with opts.frame as input when init_done called
93
cb : undefined # opts.cb(undefined, this object)
94
95
@init_eval_note()
96
opts.cb?(undefined, @)
97
# window.w = @ # for debugging
98
99
# client code should call this when start adding objects to the scene
100
init: () =>
101
if @_init
102
return
103
@_init = true
104
105
@_id = misc.uuid()
106
@init_aspect_ratio_functions()
107
108
@scene = new THREE.Scene()
109
110
# IMPORTANT: There is a major bug in three.js -- if you make the width below more than .5 of the window
111
# width, then after 8 3d renders, things get foobared in WebGL mode. This happens even with the simplest
112
# demo using the basic cube example from their site with R68. It even sometimes happens with this workaround, but
113
# at least retrying a few times can fix it.
114
if not @opts.width? or @opts.width < MIN_WIDTH
115
# ignore width/height less than a cutoff -- some graphics,
116
# e.g., "Polyhedron([(0,0,0),(0,1,0),(0,2,1),(1,0,0),(1,2,3),(2,1,1)]).plot()"
117
# weirdly set it very small.
118
@opts.width = $(window).width()*.5
119
120
@opts.height = if @opts.height? and @opts.height >= MIN_HEIGHT then @opts.height else @opts.width*2/3
121
@opts.container.css(width:"#{@opts.width+50}px")
122
123
@set_dynamic_renderer()
124
@init_orbit_controls()
125
@init_on_mouseover()
126
127
# add a bunch of lights
128
@init_light()
129
130
# set background color
131
@opts.element.find(".webapp-3d-canvas").css('background':@opts.background)
132
133
if not @opts.foreground?
134
if @opts.background == 'transparent'
135
@opts.foreground = 'gray'
136
else
137
c = @opts.element.find(".webapp-3d-canvas").css('background')
138
if not c? or c.indexOf(')') == -1
139
@opts.foreground = "#000" # e.g., on firefox - this is best we can do for now
140
else
141
i = c.indexOf(')')
142
z = []
143
for a in c.slice(4,i).split(',')
144
b = parseInt(a)
145
if b < 128
146
z.push(255)
147
else
148
z.push(0)
149
@opts.foreground = rgb_to_hex(z[0], z[1], z[2])
150
151
# client code should call this when done adding objects to the scene
152
init_done: () =>
153
if @opts.frame?
154
@set_frame(@opts.frame)
155
156
if @renderer_type != 'dynamic'
157
# if we don't have the renderer, swap it in, make a static image, then give it back to whoever had it.
158
owner = _scene_using_renderer
159
@set_dynamic_renderer()
160
@set_static_renderer()
161
owner?.set_dynamic_renderer()
162
163
# possibly show the canvas warning.
164
if dynamic_renderer_type == 'canvas'
165
@opts.element.find(".webapp-3d-canvas-warning").show().tooltip()
166
167
# show an "eval note" if we don't load the scene within a second.
168
init_eval_note: () =>
169
f = () =>
170
if not @_init
171
@opts.element.find(".webapp-3d-note").show()
172
setTimeout(f, 1000)
173
174
set_dynamic_renderer: () =>
175
# console.log "dynamic renderer"
176
if @renderer_type == 'dynamic'
177
# already have it
178
return
179
@renderer = get_renderer(@, @opts.renderer)
180
@renderer_type = 'dynamic'
181
# place renderer in correct place in the DOM
182
@opts.element.find(".webapp-3d-canvas").empty().append($(@renderer.domElement))
183
if @opts.background == 'transparent'
184
@renderer.setClearColor(0x000000, 0)
185
else
186
@renderer.setClearColor(@opts.background, 1)
187
@renderer.setSize(@opts.width, @opts.height)
188
if @controls?
189
@controls.enabled = true
190
if @last_canvas_pos?
191
@controls.object.position.copy(@last_canvas_pos)
192
if @last_canvas_target?
193
@controls.target.copy(@last_canvas_target)
194
if @opts.spin
195
@animate(render:false)
196
@render_scene(true)
197
198
set_static_renderer: () =>
199
# console.log "static renderer"
200
if @renderer_type == 'static'
201
# already have it
202
return
203
@static_image = @data_url()
204
@renderer_type = 'static'
205
if @controls?
206
@controls.enabled = false
207
@last_canvas_pos = @controls.object.position
208
@last_canvas_target = @controls.target
209
img = $("<img class='webapp-3d-static-renderer'>").attr(src:@static_image).width(@opts.width).height(@opts.height)
210
@opts.element.find(".webapp-3d-canvas").empty().append(img)
211
212
# On mouseover, we switch the renderer out to use webgl, if available, and also enable spin animation.
213
init_on_mouseover: () =>
214
@opts.element.mouseenter () =>
215
@set_dynamic_renderer()
216
217
@opts.element.mouseleave () =>
218
@set_static_renderer()
219
220
@opts.element.click () =>
221
@set_dynamic_renderer()
222
223
# initialize functions to create new vectors, which take into account the scene's 3d frame aspect ratio,
224
# and also the change of coordinates from THREE.js coords to "math coordinates".
225
init_aspect_ratio_functions: () =>
226
if @opts.aspect_ratio?
227
x = @opts.aspect_ratio[0]; y = @opts.aspect_ratio[1]; z = @opts.aspect_ratio[2]
228
@vector3 = (a,b,c) => new THREE.Vector3( -y*b , x*a , z*c )
229
@vector = (v) => new THREE.Vector3( -y*v[1], x*v[0], z*v[2] )
230
@aspect_ratio_scale = (v) => [ -y*v[1], x*v[0], z*v[2] ]
231
else
232
@vector3 = (a,b,c) => new THREE.Vector3( -b , a, c )
233
@vector = (v) => new THREE.Vector3( -v[1], v[0], v[2])
234
@aspect_ratio_scale = (v) => [ -v[1], v[0], v[2]]
235
236
show_canvas: () =>
237
@init()
238
@opts.element.find(".webapp-3d-note").hide()
239
@opts.element.find(".webapp-3d-canvas").show()
240
241
data_url: (opts) =>
242
opts = defaults opts,
243
type : 'png' # 'png' or 'jpeg' or 'webp' (the best)
244
quality : undefined # 1 is best quality; 0 is worst; only applies for jpeg or webp
245
s = @renderer.domElement.toDataURL("image/#{opts.type}", opts.quality)
246
# console.log("took #{misc.to_json(opts)} snapshot (length=#{s.length})")
247
return s
248
249
init_orbit_controls: () =>
250
if not @camera?
251
@add_camera(distance:@opts.camera_distance)
252
253
# console.log 'set_orbit_controls'
254
# set up camera controls
255
@controls = new THREE.OrbitControls(@camera, @renderer.domElement)
256
@controls.damping = 2
257
@controls.enableKeys = false # see https://github.com/mrdoob/three.js/blob/master/examples/js/controls/OrbitControls.js#L962
258
@controls.zoomSpeed = 0.4
259
if @_center?
260
@controls.target = @_center
261
if @opts.spin
262
if typeof(@opts.spin) == "boolean"
263
@controls.autoRotateSpeed = 2.0
264
else
265
@controls.autoRotateSpeed = @opts.spin
266
@controls.autoRotate = true
267
268
@controls.addEventListener 'change', () =>
269
if @renderer_type=='dynamic'
270
@rescale_objects()
271
@renderer.render(@scene, @camera)
272
273
add_camera: (opts) =>
274
opts = defaults opts,
275
distance : 10
276
277
if @camera?
278
return
279
280
view_angle = 45
281
aspect = @opts.width/@opts.height
282
near = 0.1
283
far = Math.max(20000, opts.distance*2)
284
285
@camera = new THREE.PerspectiveCamera(view_angle, aspect, near, far)
286
@scene.add(@camera)
287
@camera.position.set(opts.distance, opts.distance, opts.distance)
288
@camera.lookAt(@scene.position)
289
@camera.up = new THREE.Vector3(0,0,1)
290
291
init_light: (color= 0xffffff) =>
292
293
ambient = new THREE.AmbientLight(0x404040)
294
@scene.add(ambient)
295
296
color = 0xffffff
297
d = 10000000
298
intensity = 0.5
299
300
for p in [[d,d,d], [d,d,-d], [d,-d,d], [d,-d,-d],[-d,d,d], [-d,d,-d], [-d,-d,d], [-d,-d,-d]]
301
directionalLight = new THREE.DirectionalLight(color, intensity)
302
directionalLight.position.set(p[0], p[1], p[2]).normalize()
303
@scene.add(directionalLight)
304
305
@light = new THREE.PointLight(color)
306
@light.position.set(0,d,0)
307
308
add_text: (opts) =>
309
o = defaults opts,
310
pos : [0,0,0]
311
text : required
312
fontsize : 12
313
fontface : 'Arial'
314
color : "#000000" # anything that is valid to canvas context, e.g., "rgba(249,95,95,0.7)" is also valid.
315
constant_size : true # if true, then text is automatically resized when the camera moves
316
# WARNING: if constant_size, don't remove text from scene (or if you do, note that it is slightly inefficient still.)
317
318
#console.log("add_text: #{misc.to_json(o)}")
319
@show_canvas()
320
# make an HTML5 2d canvas on which to draw text
321
width = 300 # this determines max text width; beyond this, text is cut off.
322
height = 150
323
canvas = document.createElement( 'canvas' )
324
canvas.width = width
325
canvas.height = height
326
context = canvas.getContext("2d") # get the drawing context
327
328
# set the fontsize and fix for our text.
329
context.font = "Normal " + o.fontsize + "px " + o.fontface
330
context.textAlign = 'center'
331
332
# set the color of our text
333
context.fillStyle = o.color
334
335
# actually draw the text -- right in the middle of the canvas.
336
context.fillText(o.text, width/2, height/2)
337
338
# Make THREE.js texture from our canvas.
339
texture = new THREE.Texture(canvas)
340
texture.needsUpdate = true
341
texture.minFilter = THREE.LinearFilter
342
343
# Make a material out of our texture.
344
spriteMaterial = new THREE.SpriteMaterial(map: texture)
345
346
# Make the sprite itself. (A sprite is a 3d plane that always faces the camera.)
347
sprite = new THREE.Sprite(spriteMaterial)
348
349
# Move the sprite to its position
350
p = @aspect_ratio_scale(o.pos)
351
sprite.position.set(p[0],p[1],p[2])
352
353
# If the text is supposed to stay constant size, add it to the list of constant size text,
354
# which gets resized on scene update.
355
if o.constant_size
356
if not @_text?
357
@_text = [sprite]
358
else
359
@_text.push(sprite)
360
361
# Finally add the sprite to our scene
362
@scene.add(sprite)
363
364
return sprite
365
366
add_line: (opts) =>
367
o = defaults opts,
368
points : required
369
thickness : 1
370
color : "#000000"
371
arrow_head : false
372
if o.points.length <= 1
373
# nothing to do...
374
return
375
376
@show_canvas()
377
378
if o.arrow_head
379
# Draw an arrowhead using the ArrowHelper: https://github.com/mrdoob/three.js/blob/master/src/extras/helpers/ArrowHelper.js
380
n = o.points.length - 1
381
orig = @vector(o.points[n-1])
382
p1 = @vector(o.points[n])
383
dir = new THREE.Vector3(); dir.subVectors(p1, orig)
384
length = dir.length()
385
dir.normalize()
386
headLength = Math.max(1, o.thickness/4.0) * 0.2 * length
387
headWidth = 0.2 * headLength
388
@scene.add(new THREE.ArrowHelper(dir, orig, length, o.color, headLength, headWidth))
389
390
# always render the full line, in case there are extra points, or the thickness isn't 1 (note that ArrowHelper has no line thickness option).
391
geometry = new THREE.Geometry()
392
for a in o.points
393
geometry.vertices.push(@vector(a))
394
@scene.add(new THREE.Line(geometry, new THREE.LineBasicMaterial(color:o.color, linewidth:o.thickness)))
395
396
add_point: (opts) =>
397
o = defaults opts,
398
loc : [0,0,0]
399
size : 5
400
color: "#000000"
401
@show_canvas()
402
if not @_points?
403
@_points = []
404
405
# IMPORTANT: Below we use sprites instead of the more natural/faster PointCloudMaterial.
406
# Why? Because usually people don't plot a huge number of points, and PointCloudMaterial is SQUARE.
407
# By using sprites, our points are round, which is something people really care about.
408
409
switch dynamic_renderer_type
410
411
when 'webgl'
412
width = 50
413
height = 50
414
canvas = document.createElement('canvas')
415
canvas.width = width
416
canvas.height = height
417
418
context = canvas.getContext('2d') # get the drawing context
419
centerX = width/2
420
centerY = height/2
421
radius = 25
422
423
context.beginPath()
424
context.arc(centerX, centerY, radius, 0, 2*Math.PI, false)
425
context.fillStyle = o.color
426
context.fill()
427
428
texture = new THREE.Texture(canvas)
429
texture.needsUpdate = true
430
texture.minFilter = THREE.LinearFilter
431
spriteMaterial = new THREE.SpriteMaterial(map: texture)
432
particle = new THREE.Sprite(spriteMaterial)
433
434
p = @aspect_ratio_scale(o.loc)
435
particle.position.set(p[0],p[1],p[2])
436
@_points.push([particle, o.size/200])
437
438
when 'canvas'
439
# inspired by http://mrdoob.github.io/three.js/examples/canvas_particles_random.html
440
PI2 = Math.PI * 2
441
program = (context) ->
442
context.beginPath()
443
context.arc(0, 0, 0.5, 0, PI2, true)
444
context.fill()
445
material = new THREE.SpriteCanvasMaterial
446
color : new THREE.Color(o.color)
447
program : program
448
particle = new THREE.Sprite(material)
449
p = @aspect_ratio_scale(o.loc)
450
particle.position.set(p[0],p[1],p[2])
451
@_points.push([particle, 4*o.size/@opts.width])
452
else
453
throw Error("bug -- unkown dynamic_renderer_type = #{dynamic_renderer_type}")
454
455
@scene.add(particle)
456
457
add_obj: (myobj)=>
458
@show_canvas()
459
460
if myobj.type == 'index_face_set'
461
if myobj.has_local_colors == 0
462
has_local_colors = false
463
else
464
has_local_colors = true
465
# then we will assume that every face is a triangle or a square
466
else
467
has_local_colors = false
468
469
470
vertices = myobj.vertex_geometry
471
for objects in [0...myobj.face_geometry.length]
472
#console.log("object=", misc.to_json(myobj))
473
face3 = myobj.face_geometry[objects].face3
474
face4 = myobj.face_geometry[objects].face4
475
face5 = myobj.face_geometry[objects].face5
476
477
faces = myobj.face_geometry[objects].faces
478
if not faces?
479
faces = []
480
481
# backwards compatibility with old scenes
482
if face3?
483
for k in [0...face3.length] by 3
484
faces.push(face3.slice(k,k+3))
485
if face4?
486
for k in [0...face4.length] by 4
487
faces.push(face4.slice(k,k+4))
488
if face5?
489
for k in [0...face5.length] by 6 # yep, 6 :-()
490
faces.push(face5.slice(k,k+6))
491
492
geometry = new THREE.Geometry()
493
494
for k in [0...vertices.length] by 3
495
geometry.vertices.push(@vector(vertices.slice(k, k+3)))
496
497
push_face3 = (a, b, c) =>
498
geometry.faces.push(new THREE.Face3(a-1, b-1, c-1))
499
#geometry.faces.push(new THREE.Face3(b-1, a-1, c-1)) # both sides of faces, so material is visible from inside -- but makes some things like look really crappy; disable. Better to just set a property of the material/light, which fixes the same problem.
500
501
push_face3_with_color = (a, b, c, col) =>
502
face = new THREE.Face3(a-1, b-1, c-1)
503
face.color.setStyle(col)
504
geometry.faces.push(face)
505
#geometry.faces.push(new THREE.Face3(b-1, a-1, c-1)) # both sides of faces, so material is visible from inside -- but makes some things like look really crappy; disable. Better to just set a property of the material/light, which fixes the same problem.
506
507
# *polygonal* faces defined by 4 vertices (squares), which for THREE.js we must define using two triangles
508
push_face4 = (a, b, c, d) =>
509
push_face3(a,b,c)
510
push_face3(a,c,d)
511
512
push_face4_with_color = (a, b, c, d, col) =>
513
push_face3_with_color(a,b,c,col)
514
push_face3_with_color(a,c,d,col)
515
516
# *polygonal* faces defined by 5 vertices
517
push_face5 = (a, b, c, d, e) =>
518
push_face3(a, b, c)
519
push_face3(a, c, d)
520
push_face3(a, d, e)
521
522
# *polygonal* faces defined by 6 vertices (see http://people.cs.clemson.edu/~dhouse/courses/405/docs/brief-obj-file-format.html)
523
push_face6 = (a, b, c, d, e, f) =>
524
push_face3(a, b, c)
525
push_face3(a, c, d)
526
push_face3(a, d, e)
527
push_face3(a, e, f)
528
529
# include all faces
530
if has_local_colors
531
for v in faces
532
switch v.length
533
when 4
534
push_face3_with_color(v...)
535
when 5
536
push_face4_with_color(v...)
537
else
538
console.log("WARNING: rendering colored face with #{v.length - 1} vertices not implemented")
539
push_face4_with_color(v[0], v[1], v[2], v[3], v[-1]) # might as well render most of the face...
540
else
541
for v in faces
542
switch v.length
543
when 3
544
push_face3(v...)
545
when 4
546
push_face4(v...)
547
when 5
548
push_face5(v...)
549
when 6
550
push_face6(v...)
551
else
552
console.log("WARNING: rendering face with #{v.length} vertices not implemented")
553
push_face6(v...) # might as well render most of the face...
554
555
geometry.mergeVertices()
556
#geometry.computeCentroids()
557
geometry.computeFaceNormals()
558
#geometry.computeVertexNormals()
559
geometry.computeBoundingSphere()
560
561
#finding material key(mk)
562
name = myobj.face_geometry[objects].material_name
563
mk = 0
564
for item in [0..myobj.material.length-1]
565
if name == myobj.material[item].name
566
mk = item
567
break
568
569
if @opts.wireframe or myobj.wireframe
570
if myobj.color
571
color = myobj.color
572
else
573
c = myobj.material[mk].color
574
color = "rgb(#{c[0]*255},#{c[1]*255},#{c[2]*255})"
575
if typeof myobj.wireframe == 'number'
576
line_width = myobj.wireframe
577
else if typeof @opts.wireframe == 'number'
578
line_width = @opts.wireframe
579
else
580
line_width = 1
581
582
material = new THREE.MeshBasicMaterial
583
wireframe : true
584
color : color
585
wireframeLinewidth : line_width
586
side : THREE.DoubleSide
587
else if not myobj.material[mk]?
588
console.log("BUG -- couldn't get material for ", myobj)
589
material = new THREE.MeshBasicMaterial
590
wireframe : false
591
color : "#000000"
592
else
593
594
m = myobj.material[mk]
595
596
if has_local_colors
597
material = new THREE.MeshPhongMaterial
598
shininess : "1"
599
wireframe : false
600
transparent : m.opacity < 1
601
vertexColors: THREE.FaceColors
602
else
603
material = new THREE.MeshPhongMaterial
604
shininess : "1"
605
wireframe : false
606
transparent : m.opacity < 1
607
material.color.setRGB(m.color[0], m.color[1], m.color[2])
608
material.specular.setRGB(m.specular[0], m.specular[1], m.specular[2])
609
material.opacity = m.opacity
610
material.side = THREE.DoubleSide
611
612
mesh = new THREE.Mesh(geometry, material)
613
mesh.position.set(0,0,0)
614
@scene.add(mesh)
615
616
# always call this after adding things to the scene to make sure track
617
# controls are sorted out, etc. Set draw:false, if you don't want to
618
# actually *see* a frame.
619
set_frame: (opts) =>
620
o = defaults opts,
621
xmin : required
622
xmax : required
623
ymin : required
624
ymax : required
625
zmin : required
626
zmax : required
627
color : @opts.foreground
628
thickness : .4
629
labels : true # whether to draw three numerical labels along each of the x, y, and z axes.
630
fontsize : 14
631
draw : true
632
@show_canvas()
633
634
@_frame_params = o
635
eps = 0.1
636
x0 = o.xmin; x1 = o.xmax; y0 = o.ymin; y1 = o.ymax; z0 = o.zmin; z1 = o.zmax
637
# console.log("set_frame: #{misc.to_json(o)}")
638
if Math.abs(x1-x0)<eps
639
x1 += 1
640
x0 -= 1
641
if Math.abs(y1-y0)<eps
642
y1 += 1
643
y0 -= 1
644
if Math.abs(z1-z0)<eps
645
z1 += 1
646
z0 -= 1
647
648
mx = (x0+x1)/2
649
my = (y0+y1)/2
650
mz = (z0+z1)/2
651
@_center = @vector3(mx,my,mz)
652
653
if @camera?
654
d = 1.5*Math.max(@aspect_ratio_scale([x1-x0, y1-y0, z1-z0])...)
655
@camera.position.set(mx+d, my+d, mz+d/2)
656
# console.log("camera at #{misc.to_json([mx+d,my+d,mz+d])} pointing at #{misc.to_json(@_center)}")
657
658
if o.draw
659
if @frame?
660
# remove existing frame
661
for x in @frame
662
@scene.remove(x)
663
delete @frame
664
@frame = []
665
v = [[[x0,y0,z0], [x1,y0,z0], [x1,y1,z0], [x0,y1,z0], [x0,y0,z0],
666
[x0,y0,z1], [x1,y0,z1], [x1,y1,z1], [x0,y1,z1], [x0,y0,z1]],
667
[[x1,y0,z0], [x1,y0,z1]],
668
[[x0,y1,z0], [x0,y1,z1]],
669
[[x1,y1,z0], [x1,y1,z1]]]
670
for points in v
671
line = @add_line
672
points : points
673
color : o.color
674
thickness : o.thickness
675
@frame.push(line)
676
677
if o.draw and o.labels
678
679
if @_frame_labels?
680
for x in @_frame_labels
681
@scene.remove(x)
682
683
@_frame_labels = []
684
685
l = (a,b) ->
686
if not b?
687
z = a
688
else
689
z = (a+b)/2
690
z = z.toFixed(2)
691
return (z*1).toString()
692
693
txt = (x,y,z,text) =>
694
@_frame_labels.push(@add_text(pos:[x,y,z], text:text, fontsize:o.fontsize, color:o.color, constant_size:false))
695
696
offset = 0.15
697
if o.draw
698
e = (x1 - x0)*offset
699
txt(x0 - e, y0, z0, l(z0))
700
txt(x0 - e, y0, mz, "z = #{l(z0,z1)}")
701
txt(x0 - e, y0, z1, l(z1))
702
703
e = (x1 - x0)*offset
704
txt(x1 + e, y0, z0, l(y0))
705
txt(x1 + e, my, z0, "y = #{l(y0,y1)}")
706
txt(x1 + e, y1, z0, l(y1))
707
708
e = (y1 - y0)*offset
709
txt(x1, y0 - e, z0, l(x1))
710
txt(mx, y0 - e, z0, "x = #{l(x0,x1)}")
711
txt(x0, y0 - e, z0, l(x0))
712
713
v = @vector3(mx, my, mz)
714
@camera.lookAt(v)
715
if @controls?
716
@controls.target = @_center
717
@render_scene()
718
719
add_3dgraphics_obj: (opts) =>
720
opts = defaults opts,
721
obj : required
722
wireframe : undefined
723
set_frame : undefined
724
@show_canvas()
725
726
for o in opts.obj
727
switch o.type
728
when 'text'
729
@add_text
730
pos : o.pos
731
text : o.text
732
color : o.color
733
fontsize : o.fontsize
734
fontface : o.fontface
735
constant_size : o.constant_size
736
when 'index_face_set'
737
if opts.wireframe?
738
o.wireframe = opts.wireframe
739
@add_obj(o)
740
if o.mesh and not o.wireframe # draw a wireframe mesh on top of the surface we just drew.
741
o.color='#000000'
742
o.wireframe = o.mesh
743
@add_obj(o)
744
when 'line'
745
delete o.type
746
@add_line(o)
747
when 'point'
748
delete o.type
749
@add_point(o)
750
else
751
console.log("ERROR: no renderer for model number = #{o.id}")
752
return
753
754
if opts.set_frame?
755
@set_frame(opts.set_frame)
756
757
@render_scene(true)
758
759
760
animate: (opts={}) =>
761
opts = defaults opts,
762
fps : undefined
763
stop : false
764
mouseover : undefined # ignored now
765
render : true
766
#console.log("@animate #{@_animate_started}")
767
if @_animate_started and not opts.stop
768
return
769
@_animate_started = true
770
@_animate(opts)
771
772
_animate: (opts) =>
773
#console.log("anim?", @opts.element.length, @opts.element.is(":visible"))
774
775
if @renderer_type == 'static'
776
# will try again when we switch to dynamic renderer
777
@_animate_started = false
778
return
779
780
if not @opts.element.is(":visible")
781
if @opts.stop_when_gone? and not $.contains(document, @opts.stop_when_gone)
782
# console.log("stop_when_gone removed from document -- quit animation completely")
783
@_animate_started = false
784
else if not $.contains(document, @opts.element[0])
785
# console.log("element removed from document; wait 5 seconds")
786
setTimeout((() => @_animate(opts)), 5000)
787
else
788
# console.log("check again after a second")
789
setTimeout((() => @_animate(opts)), 1000)
790
return
791
792
if opts.stop
793
@_stop_animating = true
794
# so next time around will start
795
return
796
if @_stop_animating
797
@_stop_animating = false
798
@_animate_started = false
799
return
800
@render_scene(opts.render)
801
delete opts.render
802
f = () =>
803
requestAnimationFrame((()=>@_animate(opts)))
804
if opts.fps? and opts.fps
805
setTimeout(f , 1000/opts.fps)
806
else
807
f()
808
809
810
render_scene: (force=false) =>
811
# console.log('render', @opts.element.length)
812
# FUTURE: Render static
813
if @renderer_type == 'static'
814
console.log 'render static -- not implemented yet'
815
return
816
817
if not @camera?
818
return # nothing to do yet
819
820
@controls?.update()
821
822
pos = @camera.position
823
if not @_last_pos?
824
new_pos = true
825
@_last_pos = pos.clone()
826
else if @_last_pos.distanceToSquared(pos) > .05
827
new_pos = true
828
@_last_pos.copy(pos)
829
else
830
new_pos = false
831
832
if not new_pos and not force
833
return
834
835
# rescale all text in scene
836
@rescale_objects()
837
838
@renderer.render(@scene, @camera)
839
840
_rescale_factor: () =>
841
if not @_center?
842
return undefined
843
else
844
return @camera.position.distanceTo(@_center) / 3
845
846
rescale_objects: (force=false) =>
847
s = @_rescale_factor()
848
if not s? or (Math.abs(@_last_scale - s) < 0.000001 and not force)
849
return
850
@_last_scale = s
851
if @_text?
852
for sprite in @_text
853
sprite.scale.set(s,s,s)
854
if @_frame_labels?
855
for sprite in @_frame_labels
856
sprite.scale.set(s,s,s)
857
if @_points?
858
for z in @_points
859
c = z[1]
860
z[0].scale.set(s*c,s*c,s*c)
861
862
863
exports.render_3d_scene = (opts) ->
864
opts = defaults opts,
865
url : undefined # url from which to download (via ajax) a JSON string that parses to {opts:?,obj:?}
866
scene : undefined # {opts:?, obj:?}
867
element : required # DOM element
868
cb : undefined # cb(err, scene object)
869
# Render a 3-d scene
870
#console.log("render_3d_scene: url='#{opts.url}'")
871
872
if not opts.scene? and not opts.url?
873
opts.cb?("one of url or scene must be defined")
874
return
875
876
scene_obj = undefined
877
e = $(".webapp-3d-templates .webapp-3d-loading").clone()
878
opts.element.append(e)
879
async.series([
880
(cb) =>
881
if opts.scene?
882
cb()
883
else
884
f = (cb) ->
885
$.ajax(
886
url : opts.url
887
timeout : 30000
888
success : (data) ->
889
try
890
opts.scene = misc.from_json(data)
891
cb()
892
catch e
893
#console.log("ERROR")
894
cb(e)
895
).fail () ->
896
#console.log("FAIL")
897
cb(true)
898
misc.retry_until_success
899
f : f
900
max_tries : 10
901
max_delay : 5
902
cb : (err) ->
903
if err
904
cb("error downloading #{opts.url}")
905
else
906
cb()
907
(cb) =>
908
e.remove()
909
# do this initialization *after* we create the 3d renderer
910
init = (err, s) ->
911
if err
912
cb(err)
913
else
914
scene_obj = s
915
s.init()
916
s.add_3dgraphics_obj
917
obj : opts.scene.obj
918
s.init_done()
919
cb()
920
# create the 3d renderer
921
opts.scene.opts.cb = init
922
opts.element.webapp_threejs(opts.scene.opts)
923
], (err) ->
924
opts.cb?(err, scene_obj)
925
)
926
927
928
929
# jQuery plugin for making a DOM object into a 3d renderer
930
931
$.fn.webapp_threejs = (opts={}) ->
932
@each () ->
933
# console.log("applying official .webapp_threejs plugin")
934
elt = $(this)
935
e = $(".webapp-3d-templates .webapp-3d-viewer").clone()
936
elt.empty().append(e)
937
e.find(".webapp-3d-canvas").hide()
938
opts.element = e
939
opts.container = elt
940
941
# WARNING -- this explicit reference is brittle -- it is just an animation efficiency, but still...
942
opts.stop_when_gone = e.closest(".webapp-editor-codemirror")[0]
943
944
f = () ->
945
obj = new WebappThreeJS(opts)
946
elt.data('webapp-threejs', obj)
947
if not THREE?
948
load_threejs (err) =>
949
if not err
950
f()
951
else
952
msg = "Error loading THREE.js -- #{err}"
953
if not opts.cb?
954
console.log(msg)
955
else
956
opts.cb?(msg)
957
else
958
f()
959
960
961