;;; grid-geometry.lisp (in-package :wheelwork ) (defmacro with-grid-line ((x y) (start-x start-y) (end-x end-y) &body body) "Execute BODY for X and Y assigned to integer values in a line connecting the integer point START-X , START-Y and END-X, END-Y. " (with-gensyms (sx sy ex ey distance step progress xdiff ydiff) `(loop with ,sx = ,start-x with ,sy = ,start-y with ,ex = ,end-x with ,ey = ,end-y with ,xdiff = (- ,ex ,sx) with ,ydiff = (- ,ey, sy) with ,distance = (max (abs ,xdiff) (abs ,ydiff)) for ,step from 0 to ,distance for ,progress = (if (zerop ,distance) 0.0 (/ ,step ,distance)) for ,x = (round (+ ,start-x (* ,progress ,xdiff))) for ,y = (round (+ ,start-y (* ,progress ,ydiff))) do (progn ,@body)))) (defun grid-bbox-for (poly) "POLY is a list of pairs of xy coordinates. Return a pair of pairs, ((min-x min-y) (max-x max-y)), representing the bottom left and top right corners of the bounding box for POLY " (loop for (x y) in poly minimizing x into x-min maximizing x into x-max minimizing y into y-min maximizing y into y-max finally (return (list (list x-min y-min) (list x-max y-max))))) (defun grid-counterclockwisep (a b c) (> (* (- (first b) (first a)) (- (second c) (second a))) (* (- (second b) (second a)) (- (first c) (first a))))) (defun grid-segments-intersect-p (a b c d) (or (equalp a c) (equalp a d) (equalp b c) (equalp b d) (and (not (eq (grid-counterclockwisep a c d) (grid-counterclockwisep b c d))) (not (eq (grid-counterclockwisep a b c) (grid-counterclockwisep a b d)))))) (defun grid-poly-contains-p (poly x y) (let ((pt (list x y)) (corner (list -1 x))) (loop for (p1 p2 . more) on poly while p2 when (grid-segments-intersect-p p1 p2 pt corner) count 1 into intersection-count finally (progn (when (grid-segments-intersect-p p1 (first poly) pt corner) (incf intersection-count)) (return (oddp intersection-count)))))) (defmacro with-grid-path ((x y) (path &key autoclosep interiorp) &body body) "Interiorp assumes autoclosep." (with-gensyms (points x1 y1 x2 y2 more) (let* ((autoclose-clause (when (or interiorp autoclosep) `(destructuring-bind (,x2 ,y2) (first ,points) (with-grid-line (,x ,y) (,x2 ,y2) (,x1 ,y1) ,@body)))) (interior-clause (when interiorp `(destructuring-bind ((,x1 ,y1) (,x2 ,y2)) (grid-bbox-for ,points) (loop for ,x from ,x1 to ,x2 do (loop for ,y from ,y1 to ,y2 when (grid-poly-contains-p ,points ,x ,y) do ,@body)))))) `(let ((,points ,path)) (loop for ((,x1 ,y1) (,x2 ,y2) . ,more) on ,points while ,x2 do (with-grid-line (,x ,y) (,x1 ,y1) (,x2 ,y2) ,@body) finally (progn ,autoclose-clause ,interior-clause)))))) (defmacro with-grid-rect ((x y) (left bottom right top) &body body) `(loop for ,x from (floor ,left) to (floor ,right) do (loop for ,y from (floor ,bottom) to (floor ,top) do ,@body ))) (defmacro with-grid-circle ((x y) (cx cy radius &key interiorp) &body body) (with-gensyms (rad sx sy) (let ((comparator (if interiorp '>= '=))) `(let ((,sx ,cx) (,sy ,cy) (,rad ,radius)) (loop for ,x from (- ,sx ,rad) to (+ ,sx ,rad) do (loop for ,y from (- ,sy ,rad) to (+ ,sy ,rad) do (when (,comparator ,rad (round (euclidean-dist ,x ,y ,sx ,sy))) ,@body))))))) (defmacro with-grid-bezier ((x y) (control-pts &key (count 100)) &body body) "CONTROL-POINTS is an expression that evalueates to a list of (X Y) pairs, these are the control points for the curve.. COUNT is the number of points on the bezier curve that will be calclated. The first point is always the first control point and the last point is always the last control point. Use high counts for smooth curves, as needed. Evaluates the BODY with X Y bound to a point on the bezier curve. " (with-gensyms (fn points a step) `(let* ((,step (/ 1.0 ,count)) (,points ,control-pts) (,fn (apply #'bezier-lambda ,points))) (loop for ,a from 0.0 to (+ 1.0 ,step) by ,step for (,x ,y) = (mapcar #'round (funcall ,fn (clamp 0 ,a 1.0))) do ,@body))))