# Can Makie plots be dimension agnostic?

Makie is new to me and I’m hoping to use it to plot the results of my port of bivector.net’s C++ reference implementation of projective geometric algebra to a Julia reference implementation of projective geometric algebra. My goal is to implement in Julia a slicer for 3D printing.

Projective geometric algebra is also rather new to me. In their SIBGRAPI 2021 6 video tutorial about projective geometric algebra, Leo Dorst and Steven De Keninck show that, with some care in defining the elements, projective geometric algebra applications are “dimension agnostic” (they also use the alternative phrase “dimension independent”). For example, in Steven De Keninck’s demonstration of an inverse kinematics application, he changes the implementation (including the graphics) from a 2D PGA implementation to a 3D PGA implementation of inverse kinematics by changing a single variable (from 2 to 3) that defines the number of dimensions.

I haven’t yet looked into the javascript graphics code used in that inverse kinematics demonstration, but I’m wondering if Makie is similarly capable of being “dimension agnostic”. As an initial test, I’m wondering if Makie can

1. draw a 2D scatter plot defined by a single array in which each column defines the 2D coordinates of a point,
2. draw a 3D scatter plot defined by extending the number of rows in that array from 2 to 3,
3. make the conversion from 2D to 3D graphics with no change in code other than the change in the number of rows in the array.

In other words, is there some sort of converter from Vector{Float32} to Point2f or Point3f (depending on the length of the vector) that would allow Makie to change from a 2D scatter plot to a 3D scatter plot after changing only the number of rows in the array defining the points?

I think this is not yet possible with Makie.

The reseaon is that Makie uses a clever layouting system for composing figures with various kinds of so called `Block` elements. A `Block` element is something like an `Axis, Axis3, Colobar, Legend` etc which communicate their dimensions with one another automatically such that everything fits (almost always) nicely into one figure.

Now comes the problem: For 2D plots you draw curves into `Axis` blocks, for 3D you use `Axis3`. If you would want to just flip a switch from 2D to 3D you would have to first remove an `Axis` from the layout and then reinsert an `Axis3` in the same place. This however is not yet officially supported, I think.

@jules Or did I miss something?

However, if you really need that functionality right now we could hack a custom solution together for you. Either

1. use only a `Axis3` block and freeze the camera to a view that is orthogonal to a plane of the 3D axis to show the 2D data,
2. or implement the insertion/deletion of blocks with the non-public interface of `GridLayoutBase`. I have posted this somewhere else here on the forum.

It’s more difficult if you want to flip back and forth between 2d and 3d in an existing interactive figure, for the reasons mentioned above.

But `scatter(arr_2d)` will give you a 2d plot and `scatter(arr_3)` a 3d plot.

Fortunately, I don’t think a slicer application needs that functionality. A single 3D subplot (to show the 3D object) along side a single 2D subplot (to show a cross-section of the object) is enough.

However, there seems to be a lot of other applications of projective geometric algebra in which being dimension agnostic would be helpful, for example Steven De Keninck’s demonstration simulating the 2D physics dynamics of a wire frame square dangling on an elastic string. By changing the number of dimensions to 3, the dangling wire frame square becomes a dangling wire frame cube, and by changing the number of dimensions to 4, the dangling wire frame cube becomes a dangling wire frame tesseract.

For those other projective geometric algebra applications where dimension independence would be helpful, is it currently possible to define the number of dimensions as a GLMakie observable that triggers a whole new layout?

I mean you can always write a function that takes the dimension as an input and within the function create create a new `Figure` and setup the layout according to the dimensions.
Later on you just call the function with the dimension you want.

E.g.

``````using GLMakie
GLMakie.activate!()

function plot_something(dim)
f = Figure()
if dim == 2
ax = Axis(f[1,1])
lines!(ax, 1:3, 1:3)
elseif dim == 3
ax = Axis3(f[1,1])
lines!(ax, 1:3, 1:3, 1:3)
else
error("...")
end

return f
end

display(plot_something(2))
#display(plot_something(3))
``````

Thanks, I’ll try that … after reading through the ganja.js graphics code to see how it transforms projective geometric algebra elements to 2D and 3D coordinates.

I am still exploring how to plot projective geometric algebra (PGA) expressions but I have run into 3 basic questions about Makie, listed at the end of this post.

Hoping that the dimension of the space can be defined only once to change the “dimension agnostic” behavior of the rest of the PGA application, perhaps that one definition can be in the name of the included PGA code: ripga2d.jl for 2D and ripga3d.jl for 3D. At least that is my current approach, as shown in this initial attempt at porting (from javascript to Julia) the PGA solution to the inverse kinematics problem.

``````# file: pga2d3d_ik.jl
# interactive graphical demo of Inverse Kinematics
using GLMakie, Observables
include("ripga3d.jl")

# translate distance along line
function xlator(line::Vector{Float32},dist::Number)
return 1 - 0.5*dist*(e0*normalize(line)*!e0)
end

# inverse kinematics
armLength = 3	# max reach of robot arm
C = ["red"; "green"; "blue"] # line colors

# allocate endpoint PGA expressions
# (appended endpoint in last column is target)
EPX = Matrix{Float32}(undef, (length(e0),nEP+1))

for iEP = 1:nEP+1
EPX[:,iEP] = !(e0 + (iEP*linkLength - 1.5)*e1)
end
EPX[:,nEP+1] += 0.2*e013 # offset target point a little

# plot endpoints and target point
PTS = toPlot(EPX)
display(PTS)
fig = Figure()
ax1 = Axis(fig[1,1],
title = "initial endpoints,\n" *
"the target is separate from robot arm",
aspect=1)
ax2 = Axis(fig[1,2],
title = "endpoints after 2 steps\n" *
"of backward relaxation",
aspect= 1)
scatterlines!(ax1,
PTS[1:3,1:nEP], color="black")
scatterlines!(ax1, # target point is at end
[PTS[1,end]], [PTS[2,end]], [PTS[3,end]],
color="black")

scatterlines!(ax2,
[PTS[1,end]], [PTS[2,end]], [PTS[3,end]],
color="black")
for iRelax = 1:1 # TODO: set loop count back to 4
# set tip to target, changing length of last link
EPX[:,nEP] = EPX[:,nEP+1]
PTS = toPlot(EPX)
scatterlines!(ax2,
PTS[1:3,1:nEP], color = "black")

# restore link lengths from back to front
for jLink = 1:2 # TODO: revive loop count back to nEP-1

EPX[:,i] = XL >>> EPX[:,i+1]
PTS = toPlot(EPX)
scatterlines!(ax2,
PTS[1:3,i-1:i+1], color = C[iColor])
end
end
fig
end
``````

The resulting plot:

1. How to set the plot’s aspect ratio? There is a tutorial about aspect ratio and size control, and it clearly states “This aspect is not concerned with what the data limits are, it’s just about the relative visual length of the axes.” but I don’t see a similar tutorial about how to set the aspect ratio of data values, which I suspect is a more common concern to people plotting their data.

2. How to get the plot’s value limits? In PGA, the basis e0 defines the origin in nD+1 space and the basis e1 defines the first dimension … but not in a way I was expecting: in 2D PGA e1 is the x=0 line (i.e., the y axis) So to plot even something as simple as e1 in 2D, the limits of the plot are needed.

3. How to get a right-handed 3D coordinate system? In the plot above, all the z values are zero and the plot reverted to 2D. However when I define some z values, the resulting 3D plot has a left-handed coordinate system which is troublesome for PGA application developers because PGA is sensitive to the ordering of geometric algebra elements.

Thanks for the help. When the port from javascript to Julia finally works, I think it will help the PGA application development community as well as the Julia community.

Have a look at Axis and the sections `Controlling Axis Aspect ratios, Controlling data aspect ratios, Linking axes`.

How to get the plot’s value limits?

If by `plot's value limits` you mean the axis limits, then try

``````f = Figure()
ax = Axis(f[1,1])
((xmin, xmax), (ymin, ymax)) = Makie.limits(ax.finallimits[])
``````

How to get a right-handed 3D coordinate system?

That’s a good question. I don’t think there is currently a build-in option to do that.
There are some transformation methods, like `Makie.scale!, Makie.rotate!`, but they can only be applied to plot elements like `Lines, ScatterLines <: Makie.MakieCore.Transformables` but not to block elements like `Axis, Axis3`. (Also: Blindly applying it to `ax.blockscene` rips the whole axis apart ).
Maybe @jules has an idea for that?

Both `Axis3` and `LScene` produce right handed coordinate systems. With red x, green y, blue z:

2 Likes

Thanks, I’ll give them a try.

The 2D plots now look good. I’ve never seen bivector.net’s inverse kinematics algorithm before (even without PGA) but the following plot shows the algorithm quickly converges:

The code that generates that plot is below. I’m rather new to Julia, so if anyone notices things that would improve efficiency or style, please let me know. I’ll post the ripga3d.jl (i.e., Reference Implementation of Projective Geometric Algebra 3D) code when it is able to plot more PGA expressions than just points.

``````# pga2d3d_ik.jl
# interactive graphical demo of Inverse Kinematics
using GLMakie, Observables
include("ripga3d.jl")

# translate distance along line
function xlator(line::Vector{Float32},dist::Number)
return 1 - 0.5*dist*(e0*normalize(line)*!e0)
end

# inverse kinematics
# arguments:
# - coordinates of target of robot arm
# - number of links in robot arm
armLength = 3	# max reach of robot arm

# initialize figure
fig = Figure(resolution = (1800, 800))
C = ["red"; "green"; "blue"] # line colors
LIM = (-2,3, -2.5,2.5)
YTIC = (-2:1:2)
AX = [
Axis(fig[1,1], limits=LIM, yticks=YTIC, aspect=1,
title = "1. initial endpoints,\n" *
"the target is separate from robot arm");
Axis(fig[1,2], limits=LIM, yticks=YTIC, aspect=1,
title = "2. endpoints after 1st pass\n" *
"of backward relaxation");
Axis(fig[1,3], limits=LIM, yticks=YTIC, aspect=1,
title = "3. endpoints after 1st pass\n" *
"of forward relaxation");
Axis(fig[1,4], limits=LIM, yticks=YTIC, aspect=1,
title = "4. endpoints after 2nd pass\n" *
"of backward relaxation");
Axis(fig[1,5], limits=LIM, yticks=YTIC, aspect=1,
title = "5. endpoints after 2nd pass\n" *
"of forward relaxation");
Axis(fig[2,1], limits=LIM, yticks=YTIC, aspect=1,
title = "6. endpoints after 3rd pass\n" *
"of backward relaxation");
Axis(fig[2,2], limits=LIM, yticks=YTIC, aspect=1,
title = "7. endpoints after 3rd pass\n" *
"of forward relaxation");
Axis(fig[2,3], limits=LIM, yticks=YTIC, aspect=1,
title = "8. endpoints after 4th pass\n" *
"of backward relaxation");
Axis(fig[2,4], limits=LIM, yticks=YTIC, aspect=1,
title = "9. endpoints after 4th pass\n" *
"of forward relaxation");
]

# allocate endpoint PGA expressions
# (appended endpoint in last column is target)
PX = Matrix{Float32}(undef, (length(e0),nEP+1))

for iEP = 1:nEP
PX[:,iEP] = !(e0 + (iEP*linkLength - 1.5)*e1)
end
PX[:,nEP+1] = point(target, target, target)

# plot link endpoints and target point
P = toPlot(PX)
iAx = 1
scatterlines!(AX[iAx], P[1:3,1:nEP], color="black")
scatterlines!(AX[iAx], # target point is at end
[P[1,end]], [P[2,end]], [P[3,end]],
color="black")

# plot results of each relaxation loop
for iRelax = 1:4
# set tip to target, changing length of last link
PX[:,nEP] = PX[:,nEP+1]
P = toPlot(PX)

# restore link lengths from back to front
iAx += 1
scatterlines!(AX[iAx],
P[1:3,1:nEP], color = "light gray")
PX[:,i] = XL >>> PX[:,i+1]
P = toPlot(PX)
if i > 2
scatterlines!(AX[iAx],
P[1:3,i:i+1], color = C[iColor])
else
scatterlines!(AX[iAx],
P[1:3,i-1:i+1], color = C[iColor])
end
end

# restore link lengths from front to back
iAx += 1
scatterlines!(AX[iAx],
P[1:3,1:nEP], color = "light gray")
for i = 2:nEP
iColor = mod(i-2,3) + 1
PX[:,i] = XL >>> PX[:,i-1]
P = toPlot(PX)
scatterlines!(AX[iAx],
P[1:3,i-1:i], color = C[iColor])
end
end
fig
end
``````

Thanks for clarifying.

Somehow I was tricked into thinking that the intersection of the three back panels is the coordinate origin But adding arrows makes it clearer Is CGAL helpful for the OP’ question?

Axis3 is rotated differently than the LScene axis because I assumed it was more natural to expect the xy plane to stay similar to Axis in that x increases to the right and y increases to the top, z then increases upwards. For me, LScene is impractical in that regard, maybe we should decide on one behavior.

Hoping to show GLMakie can do something similar to ganja.js in displaying an interactive demonstration of the projective geometric algebra inverse kinematics algorithm (in the demonstration, a user drags around the target and the robot arm follows), I added a function iik() (i.e., interactive inverse kinematics) and it works:

`````` # pga2d3d_ik.jl
# interactive graphical demo of Inverse Kinematics
using GLMakie
include("ripga3d.jl")

# translate distance along line
function xlator(line::Vector{Float32},dist::Number)
#	return 1 - dist/2*(e0*normalize(line)*!e0)
return ga"1 - dist/2 (e0 normalize(line) e0∗)"
end

# inverse kinematics algorithm; plot of convergence rate
# arguments:
# - coordinates of target
# - number of links in robot arm
armLength = 3	# max reach of robot arm

# initialize figure
fig = Figure(resolution = (1800, 800))
C = ["red"; "green"; "blue"] # line colors
LIM = (-2,3, -2.5,2.5)
YTIC = (-2:1:2)
AX = [
Axis(fig[1,1], limits=LIM, yticks=YTIC, aspect=1,
title = "1. initial endpoints,\n" *
"the target is separate from robot arm");
Axis(fig[1,2], limits=LIM, yticks=YTIC, aspect=1,
title = "2. endpoints after 1st pass\n" *
"of backward relaxation");
Axis(fig[1,3], limits=LIM, yticks=YTIC, aspect=1,
title = "3. endpoints after 1st pass\n" *
"of forward relaxation");
Axis(fig[1,4], limits=LIM, yticks=YTIC, aspect=1,
title = "4. endpoints after 2nd pass\n" *
"of backward relaxation");
Axis(fig[1,5], limits=LIM, yticks=YTIC, aspect=1,
title = "5. endpoints after 2nd pass\n" *
"of forward relaxation");
Axis(fig[2,2], limits=LIM, yticks=YTIC, aspect=1,
title = "6. endpoints after 3rd pass\n" *
"of backward relaxation");
Axis(fig[2,3], limits=LIM, yticks=YTIC, aspect=1,
title = "7. endpoints after 3rd pass\n" *
"of forward relaxation");
Axis(fig[2,4], limits=LIM, yticks=YTIC, aspect=1,
title = "8. endpoints after 4th pass\n" *
"of backward relaxation");
Axis(fig[2,5], limits=LIM, yticks=YTIC, aspect=1,
title = "9. endpoints after 4th pass\n" *
"of forward relaxation");
]

# allocate endpoint PGA expressions
# (appended endpoint in last column is target)
PX = Matrix{Float32}(undef, (length(e0),nEP+1))

for iEP = 1:nEP
#		PX[:,iEP] = !(e0 + (iEP*linkLength - 1.5)*e1)
PX[:,iEP] = ga"(e0 + (iEP linkLength - 1.5) e1)∗"
end
PX[:,nEP+1] = point(target, target, target)

# plot link endpoints and target point
P = toPlot(PX)
iAx = 1
scatterlines!(AX[iAx], P[1:3,1:nEP], color="black")
scatterlines!(AX[iAx], # target point is at end
[P[1,end]], [P[2,end]], [P[3,end]],
color="black")

# plot results of each relaxation loop
for iRelax = 1:4
# set tip to target, changing length of last link
PX[:,nEP] = PX[:,nEP+1]
P = toPlot(PX)

# restore link lengths from back to front
iAx += 1
scatterlines!(AX[iAx],
P[1:3,1:nEP], color = "light gray")
#			XL = xlator( # define translation along line
#			PX[:,i] = XL >>> PX[:,i+1] # perform translation
XL = xlator(ga"PX[:,i+1] ∨ PX[:,i]", linkLength)
PX[:,i] = ga"XL PX[:,i+1] ~XL" # perform translation
P = toPlot(PX)
if i > 2
scatterlines!(AX[iAx],
P[1:3,i:i+1], color = C[iColor])
else
scatterlines!(AX[iAx],
P[1:3,i-1:i+1], color = C[iColor])
end
end

# restore link lengths from front to back
iAx += 1
scatterlines!(AX[iAx],
P[1:3,1:nEP], color = "light gray")
for i = 2:nEP
iColor = mod(i-2,3) + 1
#			XL = xlator( # define translation along line
#			PX[:,i] = XL >>> PX[:,i-1] # perform translation
XL = xlator(ga"PX[:,i-1] ∨ PX[:,i]", linkLength)
PX[:,i] = ga"XL PX[:,i-1] ~XL" # perform translation
P = toPlot(PX)
scatterlines!(AX[iAx],
P[1:3,i-1:i], color = C[iColor])
end
end
fig
end

nEP = size(PX,2) - 1 # -1 because last column is target

# for each relaxation pass
for iRelax = 1:4
# set tip to target, changing length of last link
PX[:,nEP] = PX[:,nEP+1]

# restore link lengths from back to front
#			XL = xlator( # define translation along line
#			PX[:,i] = XL >>> PX[:,i+1] # perform translation
XL = xlator(ga"PX[:,i+1] ∨ PX[:,i]", linkLength)
PX[:,i] = ga"XL PX[:,i+1] ~XL" # perform translation
end

# restore link lengths from front to back
for i = 2:nEP
#			XL = xlator( # define translation along line
#			PX[:,i] = XL >>> PX[:,i-1] # perform translation
XL = xlator(ga"PX[:,i-1] ∨ PX[:,i]", linkLength)
PX[:,i] = ga"XL PX[:,i-1] ~XL" # perform translation
end
end
end

# interactive inverse kinematics
# arguments:
# - coordinates of target of robot arm
# - number of links in robot arm
armLength = 3	# max reach of robot arm

# initialize figure
fig = Figure(resolution = (800, 800))
LIM = (-2,3, -2.5,2.5)
YTIC = (-2:1:2)
ax1 = Axis(fig[1,1], limits=LIM, yticks=YTIC, aspect=1,
title = "Interactive demonstration of inverse kinematics algorithm.\n" *
"(The target point is initially separated from the robot arm.\n" *
"Drag that target point around to see how the robot arm reacts.)")

# allocate and define endpoint PGA expressions
# (appended endpoint in last column is target)
PX = Matrix{Float32}(undef, (length(e0),nEP+1))
ANCHOR = [-1; 0; 0]
for iEP = 1:nEP
#		PX[:,iEP] = !(e0 + ((iEP-1)*linkLength + ANCHOR)*e1)
PX[:,iEP] = ga"(e0 + ((iEP-1) linkLength + ANCHOR) e1)∗"
end
PX[:,nEP+1] = point(target, target, target)

# calculate inverse kinematics
P = toPlot(PX) # convert PGA expressions to Euclidean coordinates

# define observables for plotting
RDATA = Observable(P[1:3,1:nEP]) # robot coordinates
TDATA = Observable([ANCHOR P[1:3,end]]) # target coordinates

# plot robot and target coordinates
scatterlines!(ax1, RDATA, color="black")
scatter!(ax1, TDATA, color="red")

deregister_interaction!(ax1, :rectanglezoom)
register_interaction!(ax1, :my_mouse_interaction) do event::MouseEvent, axis
if Makie.is_mouseinside(ax1.scene)
if event.type === MouseEventTypes.leftdrag
PX[:,end] = point(event.data, event.data, 0)
P = toPlot(PX)
RDATA[] = P[1:3,1:nEP] # update the plotted observables to
TDATA[] = [ANCHOR P[1:3,end]] # automatically update the plot
end
end
end
fig
end
``````

However, after reading this recent post, I realized that I too was unknowingly using a very old version (0.4.4) of GLMakie:

``````(@v1.8) pkg> status GLMakie
Status `C:\Users\gsgm2\.julia\environments\v1.8\Project.toml`
⌃ [e9467ef8] GLMakie v0.4.4

julia> Pkg.update()
Updating registry at `C:\Users\gsgm2\.julia\registries\General.toml`
Installed GR_jll ─ v0.71.3+0
Installed PyCall ─ v1.95.0
Installed Plots ── v1.38.1
Installed GR ───── v0.71.3
Updating `C:\Users\gsgm2\.julia\environments\v1.8\Project.toml`
[91a5bcdd] ↑ Plots v1.38.0 ⇒ v1.38.1
Updating `C:\Users\gsgm2\.julia\environments\v1.8\Manifest.toml`
[28b8d3ca] ↑ GR v0.71.2 ⇒ v0.71.3
[91a5bcdd] ↑ Plots v1.38.0 ⇒ v1.38.1
[438e738f] ↑ PyCall v1.94.1 ⇒ v1.95.0
[d2c73de3] ↑ GR_jll v0.71.2+0 ⇒ v0.71.3+0
Building PyCall → `C:\Users\gsgm2\.julia\scratchspaces\44cfe95a-1eb2-52ea-b672-e2afdf69b78f\b32c4b415f41f10c671cba02ae3275027dea8892\build.log`
Precompiling project...
5 dependencies successfully precompiled in 44 seconds. 262 already precompiled.
[ Info: We haven't cleaned this depot up for a bit, running Pkg.gc()...
Active manifest files: 2 found
Active artifact files: 115 found
Active scratchspaces: 13 found
Deleted no artifacts, repos, packages or scratchspaces

julia> Pkg.status("GLMakie")
Status `C:\Users\gsgm2\.julia\environments\v1.8\Project.toml`
⌃ [e9467ef8] GLMakie v0.4.4
Info Packages marked with ⌃ have new versions available and may be upgradable.

julia>
``````

What is the best practice for updating to a newer version of the GLMakie package?

Usually, `Pkg.update()` should be enough.

However, it can happen that some packages are holding back others from upgrading.
This issue is often encountered when people install lots of projects into the global environment, which is the on Julia starts with (unless told to not). Your output suggests that this is the case, because your active environment is `environments\v1.8\Project.toml` (that’s the default). However, I am not 100% sure that this why it blocks the `GLMakie` upgrade (I thought there is a little symbol next to the pkg name indicating that).

Anyways, what you could also do is to move all of your inverse kinematics code into a separate package, see `] help generate` in the REPL, and then add only the needed packages in there, e.g. `] add GLMakie`. This will always pull in the latest version. Also see: 2. Getting Started · Pkg.jl

1 Like

In my JavaScript to Julia port of a 3D slicer example application, some simple way to orient/scale/offset 3D model axes would have been helpful.

The JavaScript version of the application loads a Wavefront 3D object text file that defines all the triangle vertices and the triangle faces of a model of a bunny. As the JavaScript version plots the bunny rotating, it also calculates horizontal cross sections.

The Julia/GLMake/FileIO version of the application load()-ed the bunny object on its side (because that old Wavefront object text file format defined the model with the y-axis as the azimuth but GLMakie apparently expects the z-axis to be the azimuth). I ended up writing the following script to change the azimuth axis, and also change vertex scaling and offsets.

``````# xobj.jl
# Transform a wavefront 3D object's
# - axis orientations (new y = -old z; new z = old y)
# - scale
# - offsets
function xobj(fn="xbunny.obj", scale=13.,
xOff=0.218413, yOff=-0.01927, zOff=-0.4330287)

# initialize
iLine = 0
i1 = 1
LIM = [Inf -Inf; Inf -Inf; Inf -Inf]

# open output file
open(fn, "w") do io

# for each line of input file
for line in eachline(fn[2:end])
iLine += 1

# if line does not define a vertex
if line != 'v'
println(io, line) # copy it

# else line defines a vertex
else
nChar = length(line)
print(io, "v")
iState = 0
vTmp = 0.
i1 = 3
for iChar = 3:nChar
if isspace(line[iChar]) || iChar==nChar
iState += 1
i2 = iChar==nChar ? iChar : iChar - 1
v = parse(Float32,line[i1:i2])
v *= Float32(scale)
if iState == 1 # x
v += Float32(xOff)
print(io, ' ') # prefix space
print(io, v)
if LIM[1,1] > v
LIM[1,1] = v
end
if LIM[1,2] < v
LIM[1,2] = v
end
elseif iState == 2 # y
vTmp = v
elseif iState == 3 # z
v = -v + Float32(yOff)
print(io, ' ') # prefix space
print(io, v)
if LIM[2,1] > v
LIM[2,1] = v
end
if LIM[2,2] < v
LIM[2,2] = v
end

vTmp += Float32(zOff)
print(io, ' ') # prefix space
println(io, vTmp)
if LIM[3,1] > vTmp
LIM[3,1] = vTmp
end
if LIM[3,2] < vTmp
LIM[3,2] = vTmp
end
break
end
i1 = iChar + 1
end # if character ends a field
end # for each character
end # else line defines a vertex
end # input file
display(LIM)
end # output file
end
``````

With the modified orientation of axes, GLMakie greatly simplifies this 3D slicer example application by using the azimuth parameter to rotate the object, avoiding the JavaScript application’s approach of implementing a rotor and moving a camera. (When I finally get the Julia version’s projective geometric algebra code to calculate the slices, I’ll post the code here.)

A quick follow up showing some results … The geometric algebra community is impressed by the speed and looks of GLMakie plots. I think GLMakie will be helpful to them. The above animation is a port of their 3D slicer application example from javascript (ganja.js) to Julia and GLMakie. On my laptop computer, Julia and GLMakie generated the 33 second .mp4 video in 10 seconds.

Similarly, I think some of the work from the geometric algebra community could be helpful in Makie. For example in the above animation, projective geometric algebra (PGA) easily calculates some integrals that would be tricky to calculate without PGA. Concretely,

• the area of the cross section polygon is calculated by summing the polygon edges, and
• the volume of the 3D object is calculated by summing the polygon faces.

The idea that integration can happen by adding PGA surface objects still seems like magic to me but it works. The Julia and GLMakie code (and chocolate bunny mesh) that generated the above 3D slicer animation is in example 3.2 of the Julia and Projective Geometric Algebra essay.