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)))
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: