开发者

How to make this code simpler, clearer and "more lispy"?

I want to parse the text line from the Wavefront OBJ file. Currently I am interested in "V" and "F" types only. My algorithm is as follows:

  1. check if line is not nil (otherwise step 2 would fail)
  2. drop comment after "#" and trim spaces
  3. drop prefix "v " or "f "
  4. split string to the list of elements where each element
    1. is split to the list if it is symbol like |34/76/23|
    2. is converted from the list: I take one element only, the first by default
    3. or coerced to the given type if it is atomic number already.

Here is the code:

(defun parse-line (line prefix &key (type 'single-float))
  (declare (optimize (debug 3)))
  (labels ((rfs (what)
             (read-from-string (concatenate 'string "(" what ")")))
           (unpack (str &key (char #\/) (n 0))
             (let ((*readtable* (copy-readtable))) 
               (when char ;; we make the given char a delimiter (space)
                 (set-syntax-from-char char #\Space))
               (typecase str
                 ;; string -> list of possibly symbols.
                 ;; all elements are preserved by (map). nil's are dropped
                 (string (delete-if #'null
                                    (map 'list
                                         #'unpack
                                         (rfs str))))
                 ;; symbol -> list of values
                 (symbol (unpack (rfs (symbol-name str))))
                 ;; list -> value (only the requested one)
                 (list (unpack (nth n str)))
                 ;; value -> just coerce to type
                 (number (coerce str type))))))
    (and line
         (setf line (string-trim '(#\Space #\Tab)
                                 (subseq line 0 (position #\# line))))
         (< (length prefix) (length line))
         (string= line prefix :end1 (length prefix) :end2 (length prefix))
         (setf line (subseq line (length prefix)))
         (let ((value (unpack line :char nil))) 
           (case (length value)
               (3 value)
     开发者_JS百科          (4 (values (subseq value 0 3) ;; split quad 0-1-2-3 on tri 0-1-2 + tri 0-2-3
                          (list (nth 0 value)
                                (nth 2 value)
                                (nth 3 value)))))))))

Step four (label "unpack") is kind of recursive. It is one function and can call itself three times.

Anyway, this solution seems to be clunky.

My question is: how should one solve this task with shorter and clearer code?


I would approach this in a more structured manner.

You want to parse an obj file into some sort of data structure:

(defun parse-obj-file (filespec)
  ;; todo
  )

You need to think about how the data structure returned should look. For now, let us return a list of two lists, one of the vertices, one of the faces. The parser will go through each line, determine whether it is either a vertex or a face, and then collect it into the appropriate list:

(defun parse-obj-file (filespec)
  (with-open-file (in-stream filespec
                             :direction :input)
    (loop for line = (read-line in-stream nil)
          while line
          when (cl-ppcre:scan "^v " line)
          collect (parse-vertex line) into vertices
          when (cl-ppcre:scan "^f " line)
          collect (parse-face line) into faces
          finally (return (list vertices faces)))))

I used the cl-ppcre library here, but you could also use mismatch or search. You will then need to define parse-vertex and parse-face, for which cl-ppcre:split should come in quite handy.

It would perhaps also be useful to define classes for vertices and faces.

Update: This is how I would approach vertices:

(defclass vertex ()
  ((x :accessor x :initarg :x)
   (y :accessor y :initarg :y)
   (z :accessor z :initarg :z)
   (w :accessor w :initarg :w)))

(defun parse-vertex (line)
  (destructuring-bind (label x y z &optional w)
      (cl-ppcre:split "\\s+" (remove-comment line))
    (declare (ignorable label))
    (make-instance 'vertex
                   :x (parse-number x)
                   :y (parse-number y)
                   :z (parse-number z)
                   :w (parse-number w))))

Parse-number is from the parse-number library. It is better than using read.

Update 2: (Sorry for making this a run-on story; I have to interlace some work.) A face consists of a list of face-points.

(defclass face-point ()
  ((vertex-index :accessor vertex-index :initarg :vertex-index)
   (texture-coordinate :accessor texture-coordinate
                       :initarg :texture-coordinate)
   (normal :accessor normal :initarg :normal)))

(defun parse-face (line)
  (destructuring-bind (label &rest face-points)
      (cl-ppcre:split "\\s+" (remove-comment line))
    (declare (ignorable label))
    (mapcar #'parse-face-point face-points)))

(defun parse-face-point (string)
  (destructuring-bind (vertex-index &optional texture-coordinate normal)
      (cl-ppcre:split "/" string)
    (make-instance 'face-point
                   :vertex-index vertex-index
                   :texture-coordinate texture-coordinate
                   :normal normal)))

Remove-comment simply throws away everything after the first #:

(defun remove-comment (line)
  (subseq line 0 (position #\# line)))
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜