︠ac8f3cfe-85a6-456d-ba09-1d8aa2ade744s︠ g = tetrahedron(color='purple', mesh=3) g ︡ed0b40af-5a6f-40ea-b426-7955e6409a10︡{"file":{"uuid":"f7a374f5-4738-4abe-beec-641ab335a710","filename":"f7a374f5-4738-4abe-beec-641ab335a710.sage3d"}}︡ ︠850ceaed-a27d-4046-b1bd-de0d580ef180s︠ import graphics graphics.graphics3d_to_jsonable(g) ︡e8973dc7-b3cf-4e31-a5cb-a4dc8a2dedf2︡{"stdout":"[{'mesh': 3.0, 'material': [{'opacity': 1.0, 'illum': 1.0, 'specular': [0.0, 0.0, 0.0], 'name': 'texture3', 'ambient': [0.250980392157, 5e-06, 0.250980392157], 'color': [0.5019607843137255, 0.0, 0.5019607843137255], 'diffuse': [0.501960784314, 1e-05, 0.501960784314], 'shininess': 1.0}], 'type': 'index_face_set', 'vertex_geometry': [0.0, 0.0, 1.0, 0.942809, 0.0, -0.333333, -0.471405, 0.816497, -0.333333, -0.471405, -0.816497, -0.333333], 'face_geometry': [{'face4': [], 'face5': [], 'material_name': 'texture3', 'face3': [1, 2, 3, 2, 4, 3, 1, 3, 4, 1, 4, 2]}]}]\n"}︡ ︠aca929ba-68e2-4a70-801a-30fe335bb661︠ %md Below I've included the current version of the program that renders the above JSON objects using three.js. It may be of use in deciding how to use that format. ︠7ca8e01c-d626-4af0-b2e1-07db4b572e3e︠ %coffeescript ############################################################################### # Copyright (c) 2013, 2014, William Stein # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ############################################################################### async = require('async') misc = require('misc') {defaults, required} = misc component_to_hex = (c) -> hex = c.toString(16); if hex.length == 1 return "0" + hex else return hex rgb_to_hex = (r, g, b) -> "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b) _loading_threejs_callbacks = [] VERSION = '68' $.ajaxSetup(cache: true) # when using getScript, cache result. load_threejs = (cb) -> if Detector? and THREE? cb(); return _loading_threejs_callbacks.push(cb) #console.log("load_threejs") if _loading_threejs_callbacks.length > 1 #console.log("load_threejs: already loading...") return # will get called later below load = (script, name, cb) -> if typeof(name) != 'string' cb = name name = undefined m = (msg) -> #console.log("load('#{script}'): #{msg}") m() if name? and not window.module? window.module = {exports:{}} # ugly hack around THREE.js now supporting modules g = $.getScript(script) g.done (script, textStatus) -> if name? window[name] = window.module.exports delete window.module # console.log("THREE=", THREE?) m("done: #{textStatus}") cb() g.fail (jqxhr, settings, exception) -> m("fail: #{exception}") if name? delete window.module cb("error loading -- #{exception}") async.series([ (cb) -> load("/static/threejs/r#{VERSION}/three.min.js", 'THREE', cb) (cb) -> load("/static/threejs/r#{VERSION}/OrbitControls.min.js", cb) (cb) -> load("/static/threejs/r#{VERSION}/Detector.min.js", cb) (cb) -> f = () -> if Detector? and THREE? cb() else #console.log("load_threejs: waiting for THREEJS...") setTimeout(f, 100) f() ], (err) -> #console.log("load_threejs: done loading") for cb in _loading_threejs_callbacks cb(err) _loading_threejs_callbacks = [] ) _scene_using_renderer = undefined _renderer = {webgl:undefined, canvas:undefined} dynamic_renderer_type = undefined get_renderer = (scene, type) -> # if there is a scene currently using this renderer, tell it to switch to # the static renderer. if _scene_using_renderer? and _scene_using_renderer._id != scene._id _scene_using_renderer.set_static_renderer() # now scene takes over using this renderer _scene_using_renderer = scene if Detector.webgl and (not type? or type == 'webgl') type = 'webgl' else type = 'canvas' dynamic_renderer_type = type if not _renderer[type]? # get the best-possible THREE.js renderer (once and for all) if type == 'webgl' _renderer[type] = new THREE.WebGLRenderer antialias : true alpha : true preserveDrawingBuffer : true else _renderer[type] = new THREE.CanvasRenderer antialias : true alpha : true $(_renderer[type].domElement).addClass("salvus-3d-dynamic-renderer") return _renderer[type] MIN_WIDTH = MIN_HEIGHT = 16 class SalvusThreeJS constructor: (opts) -> @opts = defaults opts, element : required container : required width : undefined height : undefined renderer : undefined # 'webgl' or 'canvas' or undefined to choose best background : "#fafafa" foreground : undefined spin : false # if true, image spins by itself when mouse is over it. camera_distance : 10 aspect_ratio : undefined # undefined does nothing or a triple [x,y,z] of length three, # which scales the x,y,z coordinates of everything by the given values. stop_when_gone : undefined # if given, animation, etc., stops when this html element (not jquery!) is no longer in the DOM frame : undefined # if given call set_frame with opts.frame as input when init_done called cb : undefined # opts.cb(undefined, this object) @init_eval_note() load_threejs () => opts.cb?(undefined, @) # client code should call this when start adding objects to the scene init: () => if @_init return @_init = true @_id = misc.uuid() @init_aspect_ratio_functions() @scene = new THREE.Scene() # IMPORTANT: There is a major bug in three.js -- if you make the width below more than .5 of the window # width, then after 8 3d renders, things get foobared in WebGL mode. This happens even with the simplest # demo using the basic cube example from their site with R68. It even sometimes happens with this workaround, but # at least retrying a few times can fix it. if not @opts.width? or @opts.width < MIN_WIDTH # ignore width/height less than a cutoff -- some graphics, # e.g., "Polyhedron([(0,0,0),(0,1,0),(0,2,1),(1,0,0),(1,2,3),(2,1,1)]).plot()" # weirdly set it very small. @opts.width = $(window).width()*.5 @opts.height = if @opts.height? and @opts.height >= MIN_HEIGHT then @opts.height else @opts.width*2/3 @opts.container.css(width:"#{@opts.width+50}px") @set_dynamic_renderer() @init_orbit_controls() @init_on_mouseover() # add a bunch of lights @init_light() # set background color @opts.element.find(".salvus-3d-canvas").css('background':@opts.background) if not @opts.foreground? c = @opts.element.find(".salvus-3d-canvas").css('background') if not c? or c.indexOf(')') == -1 @opts.foreground = "#000" # e.g., on firefox - this is best we can do for now else i = c.indexOf(')') z = [] for a in c.slice(4,i).split(',') b = parseInt(a) if b < 128 z.push(255) else z.push(0) @opts.foreground = rgb_to_hex(z[0], z[1], z[2]) # client code should call this when done adding objects to the scene init_done: () => if @opts.frame? @set_frame(@opts.frame) if @renderer_type != 'dynamic' # if we don't have the renderer, swap it in, make a static image, then give it back to whoever had it. owner = _scene_using_renderer @set_dynamic_renderer() @set_static_renderer() owner?.set_dynamic_renderer() # possibly show the canvas warning. if dynamic_renderer_type == 'canvas' @opts.element.find(".salvus-3d-canvas-warning").show().tooltip() # show an "eval note" if we don't load the scene within a second. init_eval_note: () => f = () => if not @_init @opts.element.find(".salvus-3d-note").show() setTimeout(f, 1000) set_dynamic_renderer: () => # console.log "dynamic renderer" if @renderer_type == 'dynamic' # already have it return @renderer = get_renderer(@, @opts.renderer) @renderer_type = 'dynamic' # place renderer in correct place in the DOM @opts.element.find(".salvus-3d-canvas").empty().append($(@renderer.domElement)) @renderer.setClearColor(@opts.background, 1) @renderer.setSize(@opts.width, @opts.height) if @controls? @controls.enabled = true if @last_canvas_pos? @controls.object.position.copy(@last_canvas_pos) if @last_canvas_target? @controls.target.copy(@last_canvas_target) if @opts.spin @animate(render:false) @render_scene(true) set_static_renderer: () => # console.log "static renderer" if @renderer_type == 'static' # already have it return @static_image = @data_url() @renderer_type = 'static' if @controls? @controls.enabled = false @last_canvas_pos = @controls.object.position @last_canvas_target = @controls.target img = $("").attr(src:@static_image).width(@opts.width).height(@opts.height) @opts.element.find(".salvus-3d-canvas").empty().append(img) # On mouseover, we switch the renderer out to use webgl, if available, and also enable spin animation. init_on_mouseover: () => @opts.element.mouseenter () => @set_dynamic_renderer() @opts.element.mouseleave () => @set_static_renderer() @opts.element.click () => @set_dynamic_renderer() # initialize functions to create new vectors, which take into account the scene's 3d frame aspect ratio. init_aspect_ratio_functions: () => if @opts.aspect_ratio? x = @opts.aspect_ratio[0]; y = @opts.aspect_ratio[1]; z = @opts.aspect_ratio[2] @vector3 = (a,b,c) => new THREE.Vector3(x*a, y*b, z*c) @vector = (v) => new THREE.Vector3(x*v[0], y*v[1], z*v[2]) @aspect_ratio_scale = (v) => [x*v[0], y*v[1], z*v[2]] else @vector3 = (a,b,c) => new THREE.Vector3(a, b, c) @vector = (v) => new THREE.Vector3(v[0],v[1],v[2]) @aspect_ratio_scale = (v) => v show_canvas: () => @init() @opts.element.find(".salvus-3d-note").hide() @opts.element.find(".salvus-3d-canvas").show() data_url: (opts) => opts = defaults opts, type : 'png' # 'png' or 'jpeg' or 'webp' (the best) quality : undefined # 1 is best quality; 0 is worst; only applies for jpeg or webp s = @renderer.domElement.toDataURL("image/#{opts.type}", opts.quality) # console.log("took #{misc.to_json(opts)} snapshot (length=#{s.length})") return s init_orbit_controls: () => if not @camera? @add_camera(distance:@opts.camera_distance) # console.log 'set_orbit_controls' # set up camera controls @controls = new THREE.OrbitControls(@camera, @renderer.domElement) @controls.damping = 2 @controls.noKeys = true @controls.zoomSpeed = 0.4 if @_center? @controls.target = @_center if @opts.spin if typeof(@opts.spin) == "boolean" @controls.autoRotateSpeed = 2.0 else @controls.autoRotateSpeed = @opts.spin @controls.autoRotate = true @controls.addEventListener 'change', () => if @renderer_type=='dynamic' @rescale_objects() @renderer.render(@scene, @camera) add_camera: (opts) => opts = defaults opts, distance : 10 if @camera? return view_angle = 45 aspect = @opts.width/@opts.height near = 0.1 far = Math.max(20000, opts.distance*2) @camera = new THREE.PerspectiveCamera(view_angle, aspect, near, far) @scene.add(@camera) @camera.position.set(opts.distance, opts.distance, opts.distance) @camera.lookAt(@scene.position) @camera.up = new THREE.Vector3(0,0,1) init_light: (color= 0xffffff) => ambient = new THREE.AmbientLight(0x404040) @scene.add(ambient) color = 0xffffff d = 10000000 intensity = 0.5 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]] directionalLight = new THREE.DirectionalLight(color, intensity) directionalLight.position.set(p[0], p[1], p[2]).normalize() @scene.add(directionalLight) @light = new THREE.PointLight(color) @light.position.set(0,d,0) add_text: (opts) => o = defaults opts, pos : [0,0,0] text : required fontsize : 12 fontface : 'Arial' color : "#000000" # anything that is valid to canvas context, e.g., "rgba(249,95,95,0.7)" is also valid. constant_size : true # if true, then text is automatically resized when the camera moves; # WARNING: if constant_size, don't remove text from scene (or if you do, note that it is slightly inefficient still.) #console.log("add_text: #{misc.to_json(o)}") @show_canvas() # make an HTML5 2d canvas on which to draw text width = 300 # this determines max text width; beyond this, text is cut off. height = 150 canvas = document.createElement( 'canvas' ) canvas.width = width canvas.height = height context = canvas.getContext("2d") # get the drawing context # set the fontsize and fix for our text. context.font = "Normal " + o.fontsize + "px " + o.fontface context.textAlign = 'center' # set the color of our text context.fillStyle = o.color # actually draw the text -- right in the middle of the canvas. context.fillText(o.text, width/2, height/2) # Make THREE.js texture from our canvas. texture = new THREE.Texture(canvas) texture.needsUpdate = true # Make a material out of our texture. spriteMaterial = new THREE.SpriteMaterial(map: texture) # Make the sprite itself. (A sprite is a 3d plane that always faces the camera.) sprite = new THREE.Sprite(spriteMaterial) # Move the sprite to its position p = @aspect_ratio_scale(o.pos) sprite.position.set(p[0],p[1],p[2]) # If the text is supposed to stay constant size, add it to the list of constant size text, # which gets resized on scene update. if o.constant_size if not @_text? @_text = [sprite] else @_text.push(sprite) # Finally add the sprite to our scene @scene.add(sprite) return sprite add_line : (opts) => o = defaults opts, points : required thickness : 1 color : "#000000" arrow_head : false # TODO @show_canvas() geometry = new THREE.Geometry() for a in o.points geometry.vertices.push(@vector(a)) line = new THREE.Line(geometry, new THREE.LineBasicMaterial(color:o.color, linewidth:o.thickness)) @scene.add(line) add_point: (opts) => o = defaults opts, loc : [0,0,0] size : 5 color: "#000000" @show_canvas() if not @_points? @_points = [] # IMPORTANT: Below we use sprites instead of the more natural/faster PointCloudMaterial. # Why? Because usually people don't plot a huge number of points, and PointCloudMaterial is SQUARE. # By using sprites, our points are round, which is something people really care about. switch dynamic_renderer_type when 'webgl' width = 50 height = 50 canvas = document.createElement('canvas') canvas.width = width canvas.height = height context = canvas.getContext('2d') # get the drawing context centerX = width/2 centerY = height/2 radius = 25 context.beginPath() context.arc(centerX, centerY, radius, 0, 2*Math.PI, false) context.fillStyle = o.color context.fill() texture = new THREE.Texture(canvas) texture.needsUpdate = true spriteMaterial = new THREE.SpriteMaterial(map: texture) particle = new THREE.Sprite(spriteMaterial) p = @aspect_ratio_scale(o.loc) particle.position.set(p[0],p[1],p[2]) @_points.push([particle, o.size/200]) when 'canvas' # inspired by http://mrdoob.github.io/three.js/examples/canvas_particles_random.html PI2 = Math.PI * 2 program = (context) -> context.beginPath() context.arc(0, 0, 0.5, 0, PI2, true) context.fill() material = new THREE.SpriteCanvasMaterial color : new THREE.Color(o.color) program : program particle = new THREE.Sprite(material) p = @aspect_ratio_scale(o.loc) particle.position.set(p[0],p[1],p[2]) @_points.push([particle, 4*o.size/@opts.width]) else throw "bug -- unkown dynamic_renderer_type = #{dynamic_renderer_type}" @scene.add(particle) add_obj: (myobj)=> @show_canvas() vertices = myobj.vertex_geometry for objects in [0...myobj.face_geometry.length] #console.log("object=", misc.to_json(myobj)) face3 = myobj.face_geometry[objects].face3 face4 = myobj.face_geometry[objects].face4 face5 = myobj.face_geometry[objects].face5 geometry = new THREE.Geometry() for k in [0...vertices.length] by 3 geometry.vertices.push(@vector(vertices.slice(k, k+3))) push_face3 = (a,b,c) => geometry.faces.push(new THREE.Face3(a-1,b-1,c-1)) #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. # include all faces defined by 3 vertices (triangles) for k in [0...face3.length] by 3 push_face3(face3[k], face3[k+1], face3[k+2]) # include all *polyogonal* faces defined by 4 vertices (squares), which for THREE.js we must define using two triangles push_face4 = (a,b,c,d) => push_face3(a,b,c) push_face3(a,c,d) for k in [0...face4.length] by 4 push_face4(face4[k], face4[k+1], face4[k+2], face4[k+3]) # include all *polyogonal* faces defined by 6 vertices (see http://people.cs.clemson.edu/~dhouse/courses/405/docs/brief-obj-file-format.html) for k in [0...face5.length] by 6 [a,b,c,d,e,f] = face5.slice(k, k+6) push_face3(a, b, c) push_face3(a, c, d) push_face3(a, d, e) push_face3(a, e, f) geometry.mergeVertices() #geometry.computeCentroids() geometry.computeFaceNormals() #geometry.computeVertexNormals() geometry.computeBoundingSphere() #finding material key(mk) name = myobj.face_geometry[objects].material_name mk = 0 for item in [0..myobj.material.length-1] if name == myobj.material[item].name mk = item break if @opts.wireframe or myobj.wireframe if myobj.color color = myobj.color else c = myobj.material[mk].color color = "rgb(#{c[0]*255},#{c[1]*255},#{c[2]*255})" if typeof myobj.wireframe == 'number' line_width = myobj.wireframe else if typeof @opts.wireframe == 'number' line_width = @opts.wireframe else line_width = 1 material = new THREE.MeshBasicMaterial wireframe : true color : color wireframeLinewidth : line_width side : THREE.DoubleSide else if not myobj.material[mk]? console.log("BUG -- couldn't get material for ", myobj) material = new THREE.MeshBasicMaterial wireframe : false color : "#000000" else m = myobj.material[mk] material = new THREE.MeshPhongMaterial shininess : "1" ambient : 0x0ffff wireframe : false transparent : m.opacity < 1 material.color.setRGB(m.color[0], m.color[1], m.color[2]) material.ambient.setRGB(m.ambient[0], m.ambient[1], m.ambient[2]) material.specular.setRGB(m.specular[0], m.specular[1], m.specular[2]) material.opacity = m.opacity mesh = new THREE.Mesh(geometry, material) mesh.position.set(0,0,0) @scene.add(mesh) # always call this after adding things to the scene to make sure track # controls are sorted out, etc. Set draw:false, if you don't want to # actually *see* a frame. set_frame: (opts) => o = defaults opts, xmin : required xmax : required ymin : required ymax : required zmin : required zmax : required color : @opts.foreground thickness : .4 labels : true # whether to draw three numerical labels along each of the x, y, and z axes. fontsize : 14 draw : true @show_canvas() @_frame_params = o eps = 0.1 x0 = o.xmin; x1 = o.xmax; y0 = o.ymin; y1 = o.ymax; z0 = o.zmin; z1 = o.zmax # console.log("set_frame: #{misc.to_json(o)}") if Math.abs(x1-x0) if not b? z = a else z = (a+b)/2 z = z.toFixed(2) return (z*1).toString() txt = (x,y,z,text) => @_frame_labels.push(@add_text(pos:[x,y,z], text:text, fontsize:o.fontsize, color:o.color, constant_size:false)) offset = 0.075 if o.draw e = (y1 - y0)*offset txt(x1,y0-e,z0,l(z0)) txt(x1,y0-e,mz, "z=#{l(z0,z1)}") txt(x1,y0-e,z1,l(z1)) e = (x1 - x0)*offset txt(x1+e,y0,z0,l(y0)) txt(x1+e,my,z0, "y=#{l(y0,y1)}") txt(x1+e,y1,z0,l(y1)) e = (y1 - y0)*offset txt(x1,y1+e,z0,l(x1)) txt(mx,y1+e,z0, "x=#{l(x0,x1)}") txt(x0,y1+e,z0,l(x0)) v = @vector3(mx, my, mz) @camera.lookAt(v) if @controls? @controls.target = @_center @render_scene() add_3dgraphics_obj: (opts) => opts = defaults opts, obj : required wireframe : undefined set_frame : undefined @show_canvas() for o in opts.obj switch o.type when 'text' @add_text pos : o.pos text : o.text color : o.color fontsize : o.fontsize fontface : o.fontface constant_size : o.constant_size when 'index_face_set' if opts.wireframe? o.wireframe = opts.wireframe @add_obj(o) if o.mesh and not o.wireframe # draw a wireframe mesh on top of the surface we just drew. o.color='#000000' o.wireframe = o.mesh @add_obj(o) when 'line' delete o.type @add_line(o) when 'point' delete o.type @add_point(o) else console.log("ERROR: no renderer for model number = #{o.id}") return if opts.set_frame? @set_frame(opts.set_frame) @render_scene(true) animate: (opts={}) => opts = defaults opts, fps : undefined stop : false mouseover : undefined # ignored now render : true #console.log("@animate #{@_animate_started}") if @_animate_started and not opts.stop return @_animate_started = true @_animate(opts) _animate: (opts) => #console.log("anim?", @opts.element.length, @opts.element.is(":visible")) if @renderer_type == 'static' # will try again when we switch to dynamic renderer @_animate_started = false return if not @opts.element.is(":visible") if @opts.stop_when_gone? and not $.contains(document, @opts.stop_when_gone) # console.log("stop_when_gone removed from document -- quit animation completely") @_animate_started = false else if not $.contains(document, @opts.element[0]) # console.log("element removed from document; wait 5 seconds") setTimeout((() => @_animate(opts)), 5000) else # console.log("check again after a second") setTimeout((() => @_animate(opts)), 1000) return if opts.stop @_stop_animating = true # so next time around will start return if @_stop_animating @_stop_animating = false @_animate_started = false return @render_scene(opts.render) delete opts.render f = () => requestAnimationFrame((()=>@_animate(opts))) if opts.fps? and opts.fps setTimeout(f , 1000/opts.fps) else f() render_scene: (force=false) => # console.log('render', @opts.element.length) if @renderer_type == 'static' console.log 'render static -- todo' return if not @camera? return # nothing to do yet @controls?.update() pos = @camera.position if not @_last_pos? new_pos = true @_last_pos = pos.clone() else if @_last_pos.distanceToSquared(pos) > .05 new_pos = true @_last_pos.copy(pos) else new_pos = false if not new_pos and not force return # rescale all text in scene @rescale_objects() @renderer.render(@scene, @camera) _rescale_factor: () => if not @_center? return undefined else return @camera.position.distanceTo(@_center) / 3 rescale_objects: (force=false) => s = @_rescale_factor() if not s? or (Math.abs(@_last_scale - s) < 0.000001 and not force) return @_last_scale = s if @_text? for sprite in @_text sprite.scale.set(s,s,s) if @_frame_labels? for sprite in @_frame_labels sprite.scale.set(s,s,s) if @_points? for z in @_points c = z[1] z[0].scale.set(s*c,s*c,s*c) exports.render_3d_scene = (opts) -> opts = defaults opts, url : undefined # url from which to download (via ajax) a JSON string that parses to {opts:?,obj:?} scene : undefined # {opts:?, obj:?} element : required # DOM element cb : undefined # cb(err, scene object) # Render a 3-d scene #console.log("render_3d_scene: url='#{opts.url}'") if not opts.scene? and not opts.url? opts.cb?("one of url or scene must be defined") return scene_obj = undefined async.series([ (cb) => if opts.scene? cb() else $.ajax( url : opts.url timeout : 30000 success : (data) -> try opts.scene = misc.from_json(data) cb() catch e cb(e) ).fail () -> cb("error downloading #{opts.url}") (cb) => # do this initialization *after* we create the 3d renderer init = (err, s) -> if err cb(err) else scene_obj = s s.init() s.add_3dgraphics_obj obj : opts.scene.obj s.init_done() cb() # create the 3d renderer opts.scene.opts.cb = init opts.element.salvus_threejs(opts.scene.opts) ], (err) -> opts.cb?(err, scene_obj) ) # jQuery plugin for making a DOM object into a 3d renderer $.fn.salvus_threejs = (opts={}) -> @each () -> # console.log("applying official .salvus_threejs plugin") elt = $(this) e = $(".salvus-3d-templates .salvus-3d-viewer").clone() elt.empty().append(e) e.find(".salvus-3d-canvas").hide() opts.element = e opts.container = elt # TODO/NOTE -- this explicit reference is brittle -- it is just an animation efficiency, but still... opts.stop_when_gone = e.closest(".salvus-editor-codemirror")[0] f = () -> obj = new SalvusThreeJS(opts) elt.data('salvus-threejs', obj) if not THREE? load_threejs (err) => if not err f() else msg = "Error loading THREE.js -- #{err}" if not opts.cb? console.log(msg) else opts.cb?(msg) else f() ︠77394bc9-79eb-4f4f-b88a-32fbca6a7ad0︠