Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop Cassette.jl by rewriting animation handlers #257

Merged
merged 3 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
name = "MeshCat"
uuid = "283c5d60-a78f-5afe-a0af-af636b173e11"
authors = ["Robin Deits <[email protected]>"]
version = "0.16.3"
version = "1.0.0"

ferrolho marked this conversation as resolved.
Show resolved Hide resolved
[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Cassette = "7057c7e9-c182-5462-911a-8362d720325c"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
Expand All @@ -26,7 +25,6 @@ Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
Cassette = "0.2.5, 0.3"
Colors = "0.9, 0.10, 0.11, 0.12, 0.13"
CoordinateTransformations = "0.5, 0.6"
DocStringExtensions = "0.5, 0.6, 0.7, 0.8, 0.9"
Expand Down
8 changes: 4 additions & 4 deletions notebooks/animation.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"metadata": {},
"outputs": [],
"source": [
"anim = Animation()\n",
"anim = Animation(vis)\n",
"\n",
"atframe(anim, 0) do\n",
" # within the context of atframe, calls to \n",
Expand Down Expand Up @@ -153,7 +153,7 @@
"metadata": {},
"outputs": [],
"source": [
"anim = Animation()\n",
"anim = Animation(vis)\n",
"\n",
"atframe(anim, 0) do\n",
" settransform!(vis[\"/Cameras/default\"], Translation(0, 0, 0))\n",
Expand All @@ -179,7 +179,7 @@
"metadata": {},
"outputs": [],
"source": [
"anim = Animation()\n",
"anim = Animation(vis)\n",
"\n",
"atframe(anim, 0) do\n",
" setprop!(vis[\"/Cameras/default/rotated/<object>\"], \"zoom\", 1.0)\n",
Expand Down Expand Up @@ -226,7 +226,7 @@
"metadata": {},
"outputs": [],
"source": [
"anim = Animation()\n",
"anim = Animation(vis)\n",
"atframe(anim, 0) do\n",
" setvisible!(vis[:sphere], false)\n",
" settransform!(vis[:box1], Translation(0.0, 0, 0))\n",
Expand Down
30 changes: 29 additions & 1 deletion src/MeshCat.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ using Sockets: listen, @ip_str, IPAddr, IPv4, IPv6
using Base64: base64encode
using MsgPack: MsgPack, pack
using Pkg.Artifacts: @artifact_str
import Cassette
import FFMPEG
import HTTP
import Logging
Expand Down Expand Up @@ -102,6 +101,35 @@ abstract type AbstractMaterial end
include("util.jl")
include("trees.jl")
using .SceneTrees

struct AnimationContext
animation
frame::Int
end

"""
Low-level type which manages the actual meshcat server. See [`Visualizer`](@ref)
for the public-facing interface.
"""
mutable struct CoreVisualizer
tree::SceneNode
connections::Set{HTTP.WebSockets.WebSocket}
host::IPAddr
port::Int
server::HTTP.Server
animation_contexts::Vector{AnimationContext}

function CoreVisualizer(host::IPAddr = ip"127.0.0.1", default_port=8700)
connections = Set([])
tree = SceneNode()
port = find_open_port(host, default_port, 500)
core = new(tree, connections, host, port)
core.server = start_server(core)
core.animation_contexts = AnimationContext[]
return core
end
end

include("mesh_files.jl")
include("geometry.jl")
include("objects.jl")
Expand Down
21 changes: 12 additions & 9 deletions src/animations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,19 @@ function Base.merge!(a::AnimationClip, others::AnimationClip...)
end

struct Animation
clips::Dict{Path, AnimationClip}
visualizer::CoreVisualizer
default_framerate::Int
end
clips::Dict{Path, AnimationClip}

"""
Create a new animation. See [`atframe`](@ref) to adjust object poses or properties
in that animation.
"""
Create a new animation. See [`atframe`](@ref) to adjust object poses or properties
in that animation.

$(TYPEDSIGNATURES)
"""
Animation(visualizer::CoreVisualizer; fps=30) = new(visualizer, fps, Dict())
end

$(TYPEDSIGNATURES)
"""
Animation(fps::Int=30) = Animation(Dict{Path, AnimationClip}(), fps)

"""
Merge multiple animations, storing the result in `a`.
Expand All @@ -65,6 +67,7 @@ $(TYPEDSIGNATURES)
"""
function Base.merge!(a::Animation, others::Animation...)
for other in others
@assert a.visualizer === other.visualizer
@assert a.default_framerate == other.default_framerate
merge!(merge!, a.clips, other.clips) # merge clips recursively
end
Expand All @@ -80,7 +83,7 @@ The animations may involve the same properties or different properties
(animations of the same property on the same path will have their events
interleaved). All animations must have the same framerate.
"""
Base.merge(a::Animation, others::Animation...) = merge!(Animation(a.default_framerate), a, others...)
Base.merge(a::Animation, others::Animation...) = merge!(Animation(a.visualizer; fps=a.default_framerate), a, others...)

"""
Convert the `.tar` file of still images produced by the meshcat "record" feature
Expand Down
31 changes: 7 additions & 24 deletions src/atframe.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,6 @@ end

js_position(t::Transformation) = convert(Vector, t(SVector(0., 0, 0)))

Cassette.@context AnimationCtx

function Cassette.overdub(ctx::AnimationCtx, ::typeof(settransform!), vis::Visualizer, tform::Transformation)
animation, frame = ctx.metadata
clip = getclip!(animation, vis.path)
_setprop!(clip, frame, "scale", "vector3", js_scaling(tform))
_setprop!(clip, frame, "position", "vector3", js_position(tform))
_setprop!(clip, frame, "quaternion", "quaternion", js_quaternion(tform))
end

function Cassette.overdub(ctx::AnimationCtx, ::typeof(setprop!), vis::Visualizer, prop::AbstractString, value)
animation, frame = ctx.metadata
clip = getclip!(animation, vis.path)
_setprop!(clip, frame, prop, get_property_type(prop), value)
end

function Cassette.overdub(ctx::AnimationCtx, ::typeof(setprop!), vis::Visualizer, prop::AbstractString, jstype::AbstractString, value)
animation, frame = ctx.metadata
clip = getclip!(animation, vis.path)
_setprop!(clip, frame, prop, jstype, value)
end

"""
Call the given function `f`, but intercept any `settransform!` or `setprop!` calls
and apply them to the given animation at the given frame instead.
Expand All @@ -81,7 +59,7 @@ Usage:
vis = Visualizer()
setobject!(vis[:cube], Rect(Vec(0.0, 0.0, 0.0), Vec(0.5, 0.5, 0.5)))

anim = Animation()
anim = Animation(vis)

# At frame 0, set the cube's position to be the origin
atframe(anim, 0) do
Expand All @@ -97,6 +75,11 @@ setanimation!(vis, anim)
```
"""
function atframe(f, animation::Animation, frame::Integer)
Cassette.overdub(AnimationCtx(metadata=(animation, frame)), f)
push!(animation.visualizer.animation_contexts, AnimationContext(animation, frame))
try
f()
finally
pop!(animation.visualizer.animation_contexts)
end
return animation
end
Comment on lines 77 to 85
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I finally understood how animations are implemented in MeshCat.jl thanks to these changes.

There is a stack of objects called animation contexts, which are a kind of "helper" object to keep track of which transformations and properties are applied on different objects at different frames. This works well because the methods that apply the transformations and properties are first checking for the existence of a context before applying said transformation or property to the object. I.e., if there is no animation context, we apply the transforms directly to the objects in the visualisation; otherwise, if there is an animation context, it must be because the settransform, etc. methods are being called within an atframe call, and so we apply the transforms to the last animation context in the stack instead of to the visualiser directly.

Initially, I was confused with the pop! instruction of the context. I now understand that we push! it temporarily, then apply the "state of the world" as specified in an atframe call, and finally we pop! it back so that future settransform calls are applied directly to the visualiser again rather than to some animation context.

I suppose that there are other ways of achieving this without the need for pushing and popping contexts to a stack, but I guess that doesn't really matter as this is working really well and is a nice way of implementing the logic for conditionally applying settransform! and setprop! to either an animation context or directly to the visualiser depending on whether there is a context in the stack. 👍

57 changes: 34 additions & 23 deletions src/visualizer.jl
Original file line number Diff line number Diff line change
@@ -1,24 +1,3 @@
"""
Low-level type which manages the actual meshcat server. See [`Visualizer`](@ref)
for the public-facing interface.
"""
mutable struct CoreVisualizer
tree::SceneNode
connections::Set{HTTP.WebSockets.WebSocket}
host::IPAddr
port::Int
server::HTTP.Server

function CoreVisualizer(host::IPAddr = ip"127.0.0.1", default_port=8700)
connections = Set([])
tree = SceneNode()
port = find_open_port(host, default_port, 500)
core = new(tree, connections, host, port)
core.server = start_server(core)
return core
end
end

function find_open_port(host, default_port, max_retries)
for port in default_port:(default_port + max_retries)
server = try
Expand Down Expand Up @@ -217,7 +196,15 @@ of its parents, so setting the transform of `vis[:group1]` affects the poses of
the objects at `vis[:group1][:box1]` and `vis[:group1][:box2]`.
"""
function settransform!(vis::Visualizer, tform::Transformation)
send(vis.core, SetTransform(tform, vis.path))
if !isempty(vis.core.animation_contexts)
ctx = last(vis.core.animation_contexts)
clip = getclip!(ctx.animation, vis.path)
_setprop!(clip, ctx.frame, "scale", "vector3", js_scaling(tform))
_setprop!(clip, ctx.frame, "position", "vector3", js_position(tform))
_setprop!(clip, ctx.frame, "quaternion", "quaternion", js_quaternion(tform))
else
send(vis.core, SetTransform(tform, vis.path))
end
vis
end

Expand All @@ -240,7 +227,29 @@ $(TYPEDSIGNATURES)
with the Base.setproperty! function introduced in Julia v0.7)
"""
function setprop!(vis::Visualizer, property::AbstractString, value)
send(vis.core, SetProperty(vis.path, property, value))
if !isempty(vis.core.animation_contexts)
ctx = last(vis.core.animation_contexts)
clip = getclip!(ctx.animation, vis.path)
_setprop!(clip, ctx.frame, property, get_property_type(property), value)
else
send(vis.core, SetProperty(vis.path, property, value))
end
vis
end

"""
Variation of `setprop!` which accepts an explicit type for the underlying JS property. This property type is only used within an animation context.

$(TYPEDSIGNATURES)
"""
function setprop!(vis::Visualizer, property::AbstractString, jstype::AbstractString, value)
if !isempty(vis.core.animation_contexts)
ctx = last(vis.core.animation_contexts)
clip = getclip!(ctx.animation, vis.path)
_setprop!(clip, ctx.frame, property, jstype, value)
else
send(vis.core, SetProperty(vis.path, property, value))
end
vis
end

Expand Down Expand Up @@ -275,3 +284,5 @@ For example, if you have `vis::Visualizer` with path `/meshcat/foo`, you can do
"""
Base.getindex(vis::Visualizer, path...) =
Visualizer(vis.core, joinpath(vis.path, path...))

Animation(vis::Visualizer, args...; kw...) = Animation(vis.core, args...; kw...)
4 changes: 2 additions & 2 deletions test/visualizer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,15 @@ end
end

@testset "Animation" begin
anim1 = Animation()
anim1 = Animation(vis)
atframe(anim1, 0) do
settransform!(vis[:shapes][:box], Translation(0., 0, 0))
end
atframe(anim1, 30) do
settransform!(vis[:shapes][:box], Translation(2., 0, 0) ∘ LinearMap(RotZ(π/2)))
end
setanimation!(vis, anim1)
anim2 = Animation()
anim2 = Animation(vis)
atframe(anim2, 0) do
setprop!(vis["/Cameras/default/rotated/<object>"], "zoom", 1)
end
Expand Down
Loading