diff --git a/.gitignore b/.gitignore index 5d454ed..9555aca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .ipynb_checkpoints deps/build.log Manifest.toml - diff --git a/Project.toml b/Project.toml index 382df73..407088e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,21 +1,19 @@ name = "MeshCat" uuid = "283c5d60-a78f-5afe-a0af-af636b173e11" authors = ["Robin Deits "] -version = "0.9.1" +version = "0.10.0" [deps] -AssetRegistry = "bf4720bc-e11a-5d0c-854e-bdca1663c893" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" BinDeps = "9e28174c-4ba2-5203-b857-d8d62c4213ee" -Blink = "ad839575-38b3-5650-b840-f874b8c74a25" Cassette = "7057c7e9-c182-5462-911a-8362d720325c" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" FFMPEG = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" GeometryTypes = "4d00f742-c7ba-57c2-abde-4428a4b178cb" -JSExpr = "97c1335a-c9c5-57fe-bc5d-ec35cebe8660" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MsgPack = "99f44e22-a591-53d1-9472-aa23ef4bd671" Mux = "a975b10e-0019-58db-a62f-e48ff68538c9" Parameters = "d96e819e-fc66-5662-9728-84c9c7592b0a" @@ -24,26 +22,22 @@ Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" -WebIO = "0f1e0344-ec1d-5b48-a673-e5cf874b6c29" +WebSockets = "104b5d7c-a370-577a-8038-80a2059c5097" [compat] -AssetRegistry = "0.1" BinDeps = "1" -Blink = "0.12" Cassette = "0.2.5" Colors = "0.9, 0.10, 0.11" CoordinateTransformations = "0.5" DocStringExtensions = "0.5, 0.6, 0.7, 0.8" FFMPEG = "0.2" GeometryTypes = "0.6, 0.7" -JSExpr = "0.3, 0.5" MsgPack = "1" Mux = "0.7" Parameters = "0.10, 0.11, 0.12" Requires = "0.5, 1" Rotations = "0.8, 0.9, 0.10, 0.11, 0.13" StaticArrays = "0.10, 0.11, 0.12" -WebIO = "0.8.11" julia = "0.7, 1" [extras] diff --git a/Readme.md b/Readme.md index 864d2cc..33a9cb0 100644 --- a/Readme.md +++ b/Readme.md @@ -12,6 +12,7 @@ The MeshCat viewer runs entirely in the browser, with no external dependencies. * Inside a Jupyter Notebook with [IJulia.jl](https://github.com/JuliaLang/IJulia.jl) * In a standalone window with [Blink.jl](https://github.com/JunoLab/Blink.jl) * Inside the [Juno IDE](http://junolab.org/) +* Inside the VSCode editor with the [julia-vscode](https://www.julia-vscode.org/) extension. As much as possible, MeshCat.jl tries to use existing implementations of its fundamental types. In particular, we use: diff --git a/notebooks/animation.ipynb b/notebooks/animation.ipynb index 1ca691b..12438cf 100644 --- a/notebooks/animation.ipynb +++ b/notebooks/animation.ipynb @@ -55,7 +55,7 @@ "# open(vis)\n", "\n", "## To open the visualizer inside this jupyter notebook, do: \n", - "# IJuliaCell(vis)\n", + "# render(vis)\n", "\n", "## To open this visualizer in a standalone window, do:\n", "# using Blink\n", @@ -219,13 +219,6 @@ "source": [ "# MeshCat.convert_frames_to_video(\"/home/rdeits/Downloads/meshcat_1528401494656.tar\", overwrite=true)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { @@ -234,15 +227,15 @@ "lastKernelId": null }, "kernelspec": { - "display_name": "Julia 1.1.0", + "display_name": "Julia 1.3.1", "language": "julia", - "name": "julia-1.1" + "name": "julia-1.3" }, "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", - "version": "1.1.0" + "version": "1.3.1" } }, "nbformat": 4, diff --git a/notebooks/demo.ipynb b/notebooks/demo.ipynb index e3f30bc..faac762 100644 --- a/notebooks/demo.ipynb +++ b/notebooks/demo.ipynb @@ -56,7 +56,12 @@ "metadata": {}, "outputs": [], "source": [ - "IJuliaCell(vis)" + "# The `render(vis)` function will try to render a MeshCat\n", + "# visualizer inline. In Jupyter, it will cause the visualizer\n", + "# to be displayed in the output of the current Jupyter cell. \n", + "# In Juno/VSCode, it will cause the visualizer to be displayed in\n", + "# the plot pane.\n", + "render(vis)" ] }, { @@ -92,7 +97,7 @@ "### Embed the visualizer inside this notebook\n", "\n", "```julia\n", - "IJuliaCell(vis)\n", + "render(vis)\n", "```" ] }, @@ -436,7 +441,7 @@ "outputs": [], "source": [ "using GeometryTypes: Point3f0\n", - "points = rand(Point3f0, 100000)\n", + "points = rand(Point3f0, 1000000)\n", "setobject!(vis[:pointcloud], PointCloud(points))" ] }, @@ -531,19 +536,19 @@ ], "metadata": { "@webio": { - "lastCommId": "057909cff550485386c7979bb5f0930e", - "lastKernelId": "81aa3ac9-8239-4690-9c92-5615ab42195e" + "lastCommId": null, + "lastKernelId": null }, "kernelspec": { - "display_name": "Julia 1.1.0", + "display_name": "Julia 1.3.1", "language": "julia", - "name": "julia-1.1" + "name": "julia-1.3" }, "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", - "version": "1.1.0" + "version": "1.3.1" } }, "nbformat": 4, diff --git a/src/MeshCat.jl b/src/MeshCat.jl index 81a568d..127d47e 100644 --- a/src/MeshCat.jl +++ b/src/MeshCat.jl @@ -1,12 +1,5 @@ -__precompile__() - module MeshCat -using WebIO -import Mux -import AssetRegistry -import Cassette -import FFMPEG using GeometryTypes, CoordinateTransformations using Rotations: rotation_between, Rotation, Quat using Colors: Color, Colorant, RGB, RGBA, alpha, hex @@ -14,7 +7,6 @@ using StaticArrays: StaticVector, SVector, SDiagonal, SMatrix using GeometryTypes: raw using Parameters: @with_kw using DocStringExtensions: SIGNATURES -using JSExpr: @js, @new, @var using Requires: @require using Base.Filesystem: rm using BinDeps: download_cmd, unpack_cmd @@ -23,12 +15,18 @@ using LinearAlgebra: UniformScaling, Diagonal, norm using Sockets: listen, @ip_str, IPAddr, IPv4, IPv6 using Base64: base64encode using MsgPack: MsgPack, pack +import Mux +import Logging +import Mux.WebSockets +import Cassette +import FFMPEG + import Base: delete! export Visualizer, - ViewerWindow, - IJuliaCell + IJuliaCell, + render export setobject!, settransform!, @@ -87,32 +85,13 @@ include("msgpack.jl") include("visualizer.jl") include("atframe.jl") include("arrow_visualizer.jl") +include("render.jl") include("servers.jl") +include("assets.jl") +include("integrations.jl") const VIEWER_ROOT = joinpath(@__DIR__, "..", "assets", "meshcat", "dist") -function develop_meshcat_assets(skip_confirmation=false) - meshcat_dir = abspath(joinpath(@__DIR__, "..", "assets", "meshcat")) - if !skip_confirmation - println("CAUTION: This will delete all downloaded meshcat assets and replace them with a git clone.") - println("The following path will be overwritten:") - println(meshcat_dir) - println("To undo this operation, you will need to manually remove that directory and then run `Pkg.build(\"MeshCat\")`") - print("Proceed? (y/n) ") - choice = chomp(readline()) - if isempty(choice) || lowercase(choice[1]) != 'y' - println("Canceled.") - return - end - end - println("Removing $meshcat_dir") - rm(meshcat_dir, force=true, recursive=true) - run(`git clone https://github.com/rdeits/meshcat $meshcat_dir`) - rm(joinpath(meshcat_dir, "..", "meshcat.stamp")) -end - -const ASSET_KEYS = String[] - function __init__() main_js = abspath(joinpath(VIEWER_ROOT, "main.min.js")) if !isfile(main_js) @@ -120,17 +99,8 @@ function __init__() main.min.js not found at $main_js. Please build MeshCat using `import Pkg; Pkg.build("MeshCat")`""") end - push!(ASSET_KEYS, AssetRegistry.register(main_js)) - - @require Blink="ad839575-38b3-5650-b840-f874b8c74a25" begin - function Base.open(core::CoreVisualizer, w::Blink.AtomShell.Window) - # Ensure the window is ready - Blink.js(w, "ok") - # Set its contents - Blink.body!(w, core.scope) - w - end - end + setup_integrations() end + end diff --git a/src/assets.jl b/src/assets.jl new file mode 100644 index 0000000..d441ea4 --- /dev/null +++ b/src/assets.jl @@ -0,0 +1,19 @@ +function develop_meshcat_assets(skip_confirmation=false) + meshcat_dir = abspath(joinpath(@__DIR__, "..", "assets", "meshcat")) + if !skip_confirmation + println("CAUTION: This will delete all downloaded meshcat assets and replace them with a git clone.") + println("The following path will be overwritten:") + println(meshcat_dir) + println("To undo this operation, you will need to manually remove that directory and then run `Pkg.build(\"MeshCat\")`") + print("Proceed? (y/n) ") + choice = chomp(readline()) + if isempty(choice) || lowercase(choice[1]) != 'y' + println("Canceled.") + return + end + end + println("Removing $meshcat_dir") + rm(meshcat_dir, force=true, recursive=true) + run(`git clone https://github.com/rdeits/meshcat $meshcat_dir`) + rm(joinpath(meshcat_dir, "..", "meshcat.stamp")) +end diff --git a/src/commands.jl b/src/commands.jl index b150961..7edb391 100644 --- a/src/commands.jl +++ b/src/commands.jl @@ -20,25 +20,6 @@ struct SetProperty{T} <: AbstractCommand value::T end -abstract type AbstractControl end - -struct Button <: AbstractControl - observer::Observable - name::String -end - -struct NumericControl{T} <: AbstractControl - observer::Observable - name::String - value::T - min::T - max::T -end - -struct SetControl <: AbstractCommand - control::AbstractControl -end - struct SetAnimation{A <: Animation} <: AbstractCommand animation::A play::Bool diff --git a/src/integrations.jl b/src/integrations.jl new file mode 100644 index 0000000..042e5d9 --- /dev/null +++ b/src/integrations.jl @@ -0,0 +1,17 @@ +function setup_integrations() + @require Blink="ad839575-38b3-5650-b840-f874b8c74a25" begin + function Base.open(core::CoreVisualizer, w::Blink.AtomShell.Window) + # Ensure the window is ready + Blink.js(w, "ok") + # Set its contents + Blink.loadurl(w, url(core)) + w + end + end + + @require WebIO="0f1e0344-ec1d-5b48-a673-e5cf874b6c29" begin + WebIO.render(vis::Visualizer) = WebIO.render(vis.core) + + WebIO.render(core::CoreVisualizer) = WebIO.render(MeshCat.render(core)) + end +end diff --git a/src/lowering.jl b/src/lowering.jl index ff394be..bcff4c7 100644 --- a/src/lowering.jl +++ b/src/lowering.jl @@ -293,31 +293,6 @@ function lower(cmd::SetProperty) ) end -function lower(b::Button) - Dict{String, Any}( - "name" => b.name, - "callback" => string(@js((value) -> $(b.observer)[] = [$(b.name), Date.now()])), - ) -end - -function lower(n::NumericControl) - Dict{String, Any}( - "name" => n.name, - "callback" => string(@js((val) -> $(n.observer)[] = [$(n.name), val])), - "value" => n.value, - "min" => n.min, - "max" => n.max - ) -end - -function lower(cmd::SetControl) - d = Dict{String, Any}( - "type" => "set_control", - ) - merge!(d, lower(cmd.control)) - d -end - function lower(track::AnimationTrack) Dict{String, Any}( "name" => string(".", track.name), diff --git a/src/render.jl b/src/render.jl new file mode 100644 index 0000000..2a11590 --- /dev/null +++ b/src/render.jl @@ -0,0 +1,47 @@ +struct DisplayedVisualizer + core::CoreVisualizer +end + + +DisplayedVisualizer(vis::Visualizer) = DisplayedVisualizer(vis.core) + +url(c::DisplayedVisualizer) = url(c.core) + +""" +Render a MeshCat visualizer inline in Jupyter, Juno, or VSCode. + +If this is the last command in a Jupyter notebook cell, then the +visualizer should show up automatically in the corresponding output +cell. + +If this is run from the Juno console, then the visualizer should show +up in the Juno plot pane. Likewise if this is run from VSCode with +the julia-vscode extension, then the visualizer should show up in the +Julia Plots pane. +""" +render(vis::Visualizer) = render(vis.core) +render(core::CoreVisualizer) = DisplayedVisualizer(core) + +@deprecate IJuliaCell(v::Visualizer) render(v) + +function Base.show(io::IO, + ::Union{MIME"text/html", MIME"juliavscode/html"}, + frame::DisplayedVisualizer) + wait_for_server(frame.core) + print(io, """ +
+ +
+""") +end + +function Base.show(io::IO, + ::MIME"application/prs.juno.plotpane+html", + d::DisplayedVisualizer) + wait_for_server(d.core) + print(io, """ +
+ +
+ """) +end diff --git a/src/servers.jl b/src/servers.jl index ab85318..e5c40c4 100644 --- a/src/servers.jl +++ b/src/servers.jl @@ -1,28 +1,31 @@ +using Sockets: connect -""" - open(vis::Visualizer; host::IPAddr = ip"127.0.0.1", start_browser = true, - default_port = 8700, max_retries = 500) - -Start a server for the visualizer so that it can be accessed from a browser -using the given host URL. This method will try to search for an open port, -starting at `default_port` and then trying `max_retries` additional port -numbers. If `start_browser` is true, then a new browser window will be opened. - - open(vis::Visualizer, port::Integer; host::IPAddr = ip"127.0.0.1", start_browser = true) - -Start a server for the visualizer so that it can be accessed from a browser -using the given host URL and port. If `start_browser` is true, then a new -browser window will be opened. - -""" Base.open(vis::Visualizer, args...; kw...) = open(vis.core, args...; kw...) -function Base.open(core::CoreVisualizer, port::Integer; - host::IPAddr = ip"127.0.0.1", start_browser = true) - @async WebIO.webio_serve(Mux.page("/", req -> core.scope), host, port) - url = "http://$host:$port" - @info("Serving MeshCat visualizer at $url") - start_browser && open_url(url) +function wait_for_server(core::CoreVisualizer, timeout=100) + interval = 0.25 + socket = nothing + for i in range(0, timeout, step=interval) + try + socket = connect(core.host, core.port) + sleep(interval) + break + catch e + if e isa Base.IOError + sleep(interval) + end + end + end + if socket === nothing + error("Could not establish a connection to the visualizer.") + else + close(socket) + end +end + +function Base.open(core::CoreVisualizer) + wait_for_server(core) + open_url(url(core)) end function open_url(url) @@ -40,22 +43,3 @@ function open_url(url) println(url) end end - -function Base.open(core::CoreVisualizer; - host::IPAddr=ip"127.0.0.1", default_port=8700, max_retries=500, kw...) - for port in default_port:(default_port + max_retries) - server = try - listen(host, port) - catch e - if e isa Base.IOError - continue - end - end - close(server) - # It is *possible* that a race condition could occur here, in which - # some other process grabs the given port in between the close() above - # and the open() below. But it's unlikely and would not be terribly - # damaging (the user would just have to call open() again). - return open(core, port; host=host, kw...) - end -end diff --git a/src/visualizer.jl b/src/visualizer.jl index 886ce36..6d59258 100644 --- a/src/visualizer.jl +++ b/src/visualizer.jl @@ -1,76 +1,84 @@ -mutable struct CoreVisualizer - scope::WebIO.Scope +struct CoreVisualizer tree::SceneNode - command_channel::Observable{Vector{UInt8}} - request_channel::Observable{String} - controls_channel::Observable{Vector{Any}} - controls::Dict{String, Tuple{Observable, AbstractControl}} - - function CoreVisualizer() - scope = WebIO.Scope(imports=ASSET_KEYS, outbox=Channel{Any}(Inf)) - command_channel = Observable(scope, "meshcat-command", UInt8[]) - request_channel = Observable(scope, "meshcat-request", "") - controls_channel = Observable(scope, "meshcat-controls", []) - - onimport(scope, @js function(mc) - @var element = this.dom.children[0] - this.viewer = @new mc.Viewer(element) - $request_channel[] = String(Date.now()) - window.document.body.style.margin = "0" - window.meshcat_viewer = this.viewer - end) - - onjs(command_channel, @js function(val) - this.viewer.handle_command_message(Dict(:data => val)) - end) - scope = scope(dom"div.meshcat-viewer"( - style=Dict( - :width => "100vw", - :height => "100vh", - :position => "absolute", - :left => 0, - :right => 0, - :margin => 0, - ) - )) - scope.dom.props[:style][:overflow] = "hidden" + connections::Set{Any} + host::IPAddr + port::Int + function CoreVisualizer(host::IPAddr = ip"127.0.0.1", default_port=8700) + connections = Set([]) tree = SceneNode() - controls = Dict{String, Observable}() - vis = new(scope, tree, command_channel, request_channel, controls_channel, controls) - on(request_channel) do x - send_scene(vis) - end + port = find_open_port(host, default_port, 500) + core = new(tree, connections, host, port) + start_server(core) + return core + end +end - on(controls_channel) do msg - name::String, value = msg - if haskey(vis.controls, name) - @async vis.controls[name] = value - # Base.invokelatest(setindex!, vis.controls[name], value) +function find_open_port(host, default_port, max_retries) + for port in default_port:(default_port + max_retries) + server = try + listen(host, port) + catch e + if e isa Base.IOError + continue end end - vis + close(server) + # It is *possible* that a race condition could occur here, in which + # some other process grabs the given port in between the close() above + # and the open() below. But it's unlikely and would not be terribly + # damaging (the user would just have to call open() again). + return port end end -WebIO.render(core::CoreVisualizer) = core.scope +function start_server(core::CoreVisualizer) + asset_files = Set(["index.html", "main.min.js", "main.js"]) -function WebIO.iframe(core::CoreVisualizer; height="100%", width="100%", minHeight="400px") - ifr = WebIO.iframe(core.scope) - onimport(ifr, @js function() - this.dom.style.height = "100%" - window.foo = this - this.dom.children[0].children[0].style.flexGrow = "1" - end) - style = get!(Dict, ifr.dom.props, :style) - style[:height] = height - style[:minHeight] = minHeight - style[:width] = width - style[:display] = "flex" - style[:flexDirection] = "column" - ifr + function read_asset(file) + if file in asset_files + return open(s -> read(s, String), joinpath(VIEWER_ROOT, file)) + else + return "Not found" + end + end + default = "index.html" + Mux.@app h = ( + Mux.defaults, + Mux.page("/index.html", req -> read_asset("index.html")), + Mux.page("/main.js", req -> read_asset("main.js")), + Mux.page("/main.min.js", req -> read_asset("main.min.js")), + Mux.page("/", req -> read_asset(default)), + Mux.notfound()); + Mux.@app w = ( + Mux.wdefaults, + Mux.route("/", req -> add_connection!(core, req)), + Mux.wclose, + Mux.notfound()); + @async begin + # Suppress noisy unhelpful log messages from HTTP.jl, e.g. + # https://github.com/JuliaWeb/HTTP.jl/issues/392 + Logging.with_logger(Logging.NullLogger()) do + WebSockets.serve( + WebSockets.ServerWS( + Mux.http_handler(h), + Mux.ws_handler(w), + ), core.port); + end + end + @info "MeshCat server started. You can open the visualizer by visiting the following URL in your browser:\n$(url(core))" end +function url(core::CoreVisualizer) + "http://localhost:$(core.port[])" +end + +function add_connection!(core::CoreVisualizer, req) + connection = req[:socket] + push!(core.connections, connection) + send_scene(core, connection) + wait() +end function update_tree!(core::CoreVisualizer, cmd::SetObject, data) core.tree[cmd.path].object = data @@ -88,35 +96,42 @@ function update_tree!(core::CoreVisualizer, cmd::Delete, data) end end -update_tree!(core::CoreVisualizer, cmd::SetControl, data) = nothing update_tree!(core::CoreVisualizer, cmd::SetAnimation, data) = nothing update_tree!(core::CoreVisualizer, cmd::SetProperty, data) = nothing -function send_scene(core::CoreVisualizer) +function send_scene(core::CoreVisualizer, connection) foreach(core.tree) do node if node.object !== nothing - core.command_channel[] = node.object + WebSockets.writeguarded(connection, node.object) end if node.transform !== nothing - core.command_channel[] = node.transform + WebSockets.writeguarded(connection, node.transform) end end - for (name, (obs, control)) in core.controls - send(core, SetControl(control)) +end + +function Base.write(core::CoreVisualizer, data) + for connection in core.connections + if isopen(connection) + WebSockets.writeguarded(connection, data) + else + delete!(core.connections, connection) + end end end -function send(c::CoreVisualizer, cmd::AbstractCommand) +function send(core::CoreVisualizer, cmd::AbstractCommand) data = pack(lower(cmd)) - update_tree!(c, cmd, data) - c.command_channel[] = data + update_tree!(core, cmd, data) + write(core, data) + nothing end -function Base.wait(c::CoreVisualizer) - pool = c.scope.pool - WebIO.ensure_connection(pool) - nothing +function Base.wait(core::CoreVisualizer) + while isempty(core.connections) + sleep(0.5) + end end """ @@ -146,9 +161,9 @@ execution until the browser window has opened. """ Base.wait(v::Visualizer) = wait(v.core) -IJuliaCell(vis::Visualizer; kw...) = iframe(vis.core; kw...) +# IJuliaCell(vis::Visualizer; kw...) = iframe(vis.core; kw...) -Base.show(io::IO, v::Visualizer) = print(io, "MeshCat Visualizer with path $(v.path)") +Base.show(io::IO, v::Visualizer) = print(io, "MeshCat Visualizer with path $(v.path) at $(url(v.core))") """ $(SIGNATURES) @@ -202,20 +217,6 @@ function setprop!(vis::Visualizer, property::AbstractString, value) vis end -function setcontrol!(vis::Visualizer, name::AbstractString, obs::Observable) - control = Button(vis.core.controls_channel, name) - vis.core.controls[name] = (obs, control) - send(vis.core, SetControl(control)) - vis -end - -function setcontrol!(vis::Visualizer, name::AbstractString, obs::Observable, value, min=zero(value), max=one(value)) - control = NumericControl(vis.core.controls_channel, name, value, min, max) - vis.core.controls[name] = (obs, control) - send(vis.core, SetControl(control)) - vis -end - function setanimation!(vis::Visualizer, anim::Animation; play::Bool=true, repetitions::Integer=1) cmd = SetAnimation(anim, play, repetitions) send(vis.core, cmd) diff --git a/test/runtests.jl b/test/runtests.jl index 1ca3741..6681378 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -17,8 +17,6 @@ include("util.jl") include("wait.jl") end -sleep(10) - module ModuleTest # Test for any https://github.com/JuliaLang/julia/issues/21653 # related issues when MeshCat is included in another module