Using rubik.el to make SVG last-layer diagrams from algorithms

| cubing, emacs, org

So I checked out emacs-cube, but I had a hard time figuring out how to work with the data model without getting into all the rendering because it figures "left" and "right" based on camera position. rubik.el seemed like an easier starting point. As far as I can tell, the rubik-cube-state local variable is an array with the faces specified as 6 groups of 9 integers in this order: top, front, right, back, left, bottom, with cells specified from left to right, top to bottom.

First, I wanted to recolour rubik so that it matched the setup of the Roofpig JS library I'm using for animations.

(defconst my-cubing-rubik-faces "YRGOBW")
;; make it match roofpig's default setup with yellow on top and red in front
(defconst rubik-faces [rubik-yellow
                       rubik-red
                       rubik-green
                       rubik-orange
                       rubik-blue
                       rubik-white])

Here are some functions to apply an algorithm (or actually, the inverse of the algorithm, which is useful for exploring a PLL case):

(defun my-cubing-normalize (alg)
  "Remove parentheses and clean up spaces in ALG."
  (string-trim
   (replace-regexp-in-string "[() ]+" " " alg)))

(defun my-cubing-reverse-alg (alg)
  "Reverse the given ALG."
  (mapconcat
   (lambda (step)
     (if (string-match "\\`\\([rludfsbRLUDFSBxyz]\\)\\(['i]\\)?\\'" step)
         (concat (match-string 1 step)
                 (if (match-string 2 step)
                     ""
                   "'"))
       step))
   (reverse
    (split-string (my-cubing-normalize alg) " "))
   " "))

(defun my-cubing-rubik-alg (alg)
  "Apply the reversed ALG to a solved cube.
Return the rubik.el cube state."
  (let ((reversed (my-cubing-reverse-alg alg)))
    (seq-reduce
     (lambda (cube o)
       (when (intern (format "rubik-%s"
                             (replace-regexp-in-string "'" "i" o)))
         (unless (string= o "")
           (rubik-apply-transformation
            cube
            (symbol-value
             (intern
              (format "rubik-%s"
                      (replace-regexp-in-string "'" "i" o)))))))
       cube)
     (split-string reversed " ")
     (rubik-make-initial-cube))))

Then I got the strings specifying the side colours and the top colours in the format that I needed for the SVG diagrams. I'm optimistically using number-sequence here instead of hard-coding the numbers so that I can figure out how to extend the idea for 4x4 someday.

(defun my-cubing-rubik-top-face-strings (&optional cube)
  ;; edges starting from back left
  (let ((cube (or cube rubik-cube-state)))
    (list
     (mapconcat
      (lambda (i)
        (char-to-string (elt my-cubing-rubik-faces (aref cube i))))
      (append
       (reverse (number-sequence (* 3 9) (+ 2 (* 3 9))))
       (reverse (number-sequence (* 2 9) (+ 2 (* 2 9))))
       (reverse (number-sequence (* 1 9) (+ 2 (* 1 9))))
       (reverse (number-sequence (* 4 9) (+ 2 (* 4 9))))))
     (mapconcat
      (lambda (i)
        (char-to-string (elt my-cubing-rubik-faces (aref cube i))))
      (number-sequence 0 8)))))

Then theoretically, it can make a diagram like this:

(defun my-cubing-rubik-last-layer-with-sides-from-alg (alg &optional arrows)
  (apply 'my-cubing-last-layer-with-sides
         (append
          (my-cubing-rubik-top-face-strings (my-cubing-rubik-alg alg))
          (list
           arrows))))

So I can invoke it with:

(my-cubing-rubik-last-layer-with-sides-from-alg
 "R U R' F' R U R' U' R' F R2 U' R' U'"
 '((1 7 t) (2 8 t)))
last-layer.svg

It's also nice to be able to interactively step through the algorithm. I prefer a more compact view of the undo/redo state.

;; Override undo information
(defun rubik-display-undo ()
  "Insert undo information at point."
  (cl-loop with line-str = "\nUndo: "
           for cmd in (reverse (cdr rubik-cube-undo))
           for i = 1 then (1+ i)
           do (progn
                (setq line-str (concat line-str (format "%s " (get cmd 'name))))
                (when (> (length line-str) fill-column)
                  (insert line-str)
                  (setq line-str (concat "\n" (make-string 6 ?\s)))))
           finally (insert line-str)))

;; Override redo information
(defun rubik-display-redo ()
  "Insert redo information at point."
  (cl-loop with line-str = "\nRedo: "
           for cmd in (cdr rubik-cube-redo)
           for i = 1 then (1+ i)
           do (progn
                (setq line-str (concat line-str (format "%s " (get cmd 'name))))
                (when (> (length line-str) fill-column)
                  (insert line-str)
                  (setq line-str (concat "\n" (make-string 6 ?\s)))))
           finally (insert line-str)))
  
(defun my-cubing-convert-alg-to-rubik-commands (alg)
  (mapcar
   (lambda (step)
     (intern
      (format "rubik-%s-command"
              (replace-regexp-in-string "'" "i" step))))
   (split-string (my-cubing-normalize alg) " ")))

(rubik-define-commands
  rubik-U "U" rubik-U2 "U2" rubik-Ui "U'"
  rubik-F "F" rubik-F2 "F2" rubik-Fi "F'"
  rubik-R "R" rubik-R2 "R2" rubik-Ri "R'"
  rubik-L "L" rubik-L2 "L" rubik-Li "L'"
  rubik-B "B" rubik-B2 "B" rubik-Bi "B'"
  rubik-D "D" rubik-D2 "D" rubik-Di "D'"
  rubik-x "x" rubik-x2 "x" rubik-xi "x'"
  rubik-y "y" rubik-y2 "y" rubik-yi "y'"
  rubik-z "z" rubik-z2 "z2" rubik-zi "z'")

(defun my-cubing-rubik-set-to-alg (alg)
  (interactive "MAlg: ")
  (rubik)
  (fit-window-to-buffer)
  (setq rubik-cube-state (my-cubing-rubik-alg alg))
  (setq rubik-cube-redo (append (list 'redo)
                                (my-cubing-convert-alg-to-rubik-commands
                                 alg)))
  (setq rubik-cube-undo '(undo))
  (rubik-draw-all)
  (display-buffer (current-buffer)))

And now I can combine all those pieces together in a custom Org link type that will allow me to interactively step through an algorithm if I open it within Emacs and that will export to a diagram and an animation.

(org-link-set-parameters
 "3x3"
 :follow #'my-cubing-rubik-open
 :export #'my-cubing-rubik-export)

(defun my-cubing-rubik-open (path &optional _)
  (my-cubing-rubik-set-to-alg (if (string-match "^\\(.*\\)\\?\\(.*\\)$" path)
                                  (match-string 1 path)
                                path))) 
  
(defun my-cubing-rubik-export (path _ format _)
  "Export PATH to FORMAT."
  (let (alg arrows params)
    (setq alg path)
    (when (string-match "^\\(.*\\)\\?\\(.*\\)$" path)
      (setq alg (match-string 1 path)
            params (org-protocol-convert-query-to-plist (match-string 2 path))
            arrows
            (mapcar (lambda (entry)
                      (mapcar 'string-to-number
                               (split-string entry "-"))) 
                    (split-string
                     (plist-get params :arrows) ","))))
    (concat
     (my-cubing-rubik-last-layer-with-sides-from-alg
      alg
      arrows)
     (format "<div class=\"roofpig\" data-config=\"base=PLL|alg=%s\"></div>"
             (my-cubing-normalize alg)))))

Let's try that with this F-perm, which I haven't memorized yet:

[[3x3:(R' U' F')(R U R' U')(R' F R2 U')(R' U' R U)(R' U R)?arrows=1-7,7-1,2-8,8-2]]

At some point, I'd like to change the display for rubik.el so that it uses SVGs. (Or the OpenGL hacks in https://github.com/Jimx-/emacs-gl, but that might be beyond my current ability.) In the meantime, this might be fun.

In rubik.el, M-r redoes a move and M-u undoes it. Here's what it looks like with my tweaked interface:

output-2023-02-09-15:25:42.gif
Figure 1: Animated GIF of rubik.el stepping through an F-perm
View org source for this post
You can comment with Disqus or you can e-mail me at sacha@sachachua.com.