annotate org/touch.org @ 246:63da037ce1c5

added image
author Robert McIntyre <rlm@mit.edu>
date Sun, 12 Feb 2012 14:40:58 -0700
parents 102ac596cc3f
children 4e220c8fb1ed
rev   line source
rlm@37 1 #+title: Simulated Sense of Touch
rlm@0 2 #+author: Robert McIntyre
rlm@0 3 #+email: rlm@mit.edu
rlm@37 4 #+description: Simulated touch for AI research using JMonkeyEngine and clojure.
rlm@37 5 #+keywords: simulation, tactile sense, jMonkeyEngine3, clojure
rlm@4 6 #+SETUPFILE: ../../aurellem/org/setup.org
rlm@4 7 #+INCLUDE: ../../aurellem/org/level-0.org
rlm@0 8
rlm@37 9 * Touch
rlm@0 10
rlm@226 11 Touch is critical to navigation and spatial reasoning and as such I
rlm@226 12 need a simulated version of it to give to my AI creatures.
rlm@0 13
rlm@228 14 However, touch in my virtual can not exactly correspond to human touch
rlm@228 15 because my creatures are made out of completely rigid segments that
rlm@228 16 don't deform like human skin.
rlm@228 17
rlm@228 18 Human skin has a wide array of touch sensors, each of which speciliaze
rlm@228 19 in detecting different vibrational modes and pressures. These sensors
rlm@228 20 can integrate a vast expanse of skin (i.e. your entire palm), or a
rlm@228 21 tiny patch of skin at the tip of your finger. The hairs of the skin
rlm@228 22 help detect objects before they even come into contact with the skin
rlm@228 23 proper.
rlm@228 24
rlm@228 25 Instead of measuring deformation or vibration, I surround each rigid
rlm@228 26 part with a plenitude of hair-like objects which do not interact with
rlm@228 27 the physical world. Physical objects can pass through them with no
rlm@228 28 effect. The hairs are able to measure contact with other objects, and
rlm@228 29 constantly report how much of their extent is covered. So, even though
rlm@228 30 the creature's body parts do not deform, the hairs create a margin
rlm@228 31 around those body parts which achieves a sense of touch which is a
rlm@228 32 hybrid between a human's sense of deformation and sense from hairs.
rlm@228 33
rlm@228 34 Implementing touch in jMonkeyEngine follows a different techinal route
rlm@228 35 than vision and hearing. Those two senses piggybacked off
rlm@228 36 jMonkeyEngine's 3D audio and video rendering subsystems. To simulate
rlm@228 37 Touch, I use jMonkeyEngine's physics system to execute many small
rlm@229 38 collision detections, one for each "hair". The placement of the
rlm@229 39 "hairs" is determined by a UV-mapped image which shows where each hair
rlm@229 40 should be on the 3D surface of the body.
rlm@228 41
rlm@229 42 * Defining Touch Meta-Data in Blender
rlm@229 43
rlm@245 44 Each geometry can have a single UV map which describes the position of
rlm@245 45 the "hairs" which will constitute its sense of touch. This image path
rlm@245 46 is stored under the "touch" key. The image itself is black and white,
rlm@245 47 with black meaning a hair length of 0 (no hair is present) and white
rlm@245 48 meaning a hair length of =scale=, which is a float stored under the
rlm@245 49 key "scale". I call these "hairs" /feelers/.
rlm@229 50
rlm@231 51 #+name: meta-data
rlm@0 52 #+begin_src clojure
rlm@229 53 (defn tactile-sensor-profile
rlm@229 54 "Return the touch-sensor distribution image in BufferedImage format,
rlm@229 55 or nil if it does not exist."
rlm@229 56 [#^Geometry obj]
rlm@229 57 (if-let [image-path (meta-data obj "touch")]
rlm@229 58 (load-image image-path)))
rlm@233 59
rlm@233 60 (defn tactile-scale
rlm@233 61 "Return the maximum length of a hair. All hairs are scalled between
rlm@233 62 0.0 and this length, depending on their color. Black is 0, and
rlm@233 63 white is maximum length, and everything in between is scalled
rlm@233 64 linearlly. Default scale is 0.01 jMonkeyEngine units."
rlm@233 65 [#^Geometry obj]
rlm@233 66 (if-let [scale (meta-data obj "scale")]
rlm@233 67 scale 0.1))
rlm@228 68 #+end_src
rlm@156 69
rlm@246 70 Here is an example of a UV-map which specifies the position of touch
rlm@246 71 sensors along the surface of the worm.
rlm@229 72
rlm@246 73 #+attr_html: width=755
rlm@246 74 #+caption: This is the tactile-sensor-profile for the upper segment of the worm. It defines regions of high touch sensitivity (where there are many white pixels) and regions of low sensitivity (where white pixels are sparse).
rlm@246 75 [[../images/finger-UV.png]]
rlm@234 76
rlm@233 77 * Skin Creation
rlm@238 78
rlm@238 79 =(touch-kernel)= generates the functions which implement the sense of
rlm@238 80 touch for a creature. These functions must do 6 things to obtain touch
rlm@238 81 data.
rlm@238 82
rlm@238 83 - Get the tactile profile image and scale paramaters which describe
rlm@238 84 the layout of feelers along the object's surface.
rlm@239 85 =(tactile-sensor-profile)=, =(tactile-scale)=
rlm@239 86
rlm@238 87 - Find the triangles which make up the mesh in pixel-space and in
rlm@238 88 world-space.
rlm@239 89 =(triangles)= =(pixel-triangles)=
rlm@239 90
rlm@239 91 - Find the coordinates of each pixel in pixel space. These
rlm@239 92 coordinates are used to make the touch-topology.
rlm@240 93 =(feeler-pixel-coords)=
rlm@239 94
rlm@238 95 - Find the coordinates of each pixel in world-space. These
rlm@240 96 coordinates are the origins of the feelers. =(feeler-origins)=
rlm@239 97
rlm@238 98 - Calculate the normals of the triangles in world space, and add
rlm@238 99 them to each of the origins of the feelers. These are the
rlm@238 100 normalized coordinates of the tips of the feelers.
rlm@240 101 For both of these, =(feeler-tips)=
rlm@239 102
rlm@238 103 - Generate some sort of topology for the sensors.
rlm@239 104 =(touch-topology)=
rlm@239 105
rlm@238 106
rlm@233 107 #+name: kernel
rlm@233 108 #+begin_src clojure
rlm@233 109 (in-ns 'cortex.touch)
rlm@233 110
rlm@244 111 (defn set-ray [#^Ray ray #^Matrix4f transform
rlm@244 112 #^Vector3f origin #^Vector3f tip]
rlm@243 113 ;; Doing everything locally recduces garbage collection by enough to
rlm@243 114 ;; be worth it.
rlm@243 115 (.mult transform origin (.getOrigin ray))
rlm@243 116
rlm@243 117 (.mult transform tip (.getDirection ray))
rlm@244 118 (.subtractLocal (.getDirection ray) (.getOrigin ray)))
rlm@242 119
rlm@233 120 (defn touch-kernel
rlm@234 121 "Constructs a function which will return tactile sensory data from
rlm@234 122 'geo when called from inside a running simulation"
rlm@234 123 [#^Geometry geo]
rlm@243 124 (if-let
rlm@243 125 [profile (tactile-sensor-profile geo)]
rlm@243 126 (let [ray-reference-origins (feeler-origins geo profile)
rlm@243 127 ray-reference-tips (feeler-tips geo profile)
rlm@244 128 ray-length (tactile-scale geo)
rlm@243 129 current-rays (map (fn [_] (Ray.)) ray-reference-origins)
rlm@243 130 topology (touch-topology geo profile)]
rlm@244 131 (dorun (map #(.setLimit % ray-length) current-rays))
rlm@233 132 (fn [node]
rlm@243 133 (let [transform (.getWorldMatrix geo)]
rlm@243 134 (dorun
rlm@244 135 (map (fn [ray ref-origin ref-tip]
rlm@244 136 (set-ray ray transform ref-origin ref-tip))
rlm@243 137 current-rays ray-reference-origins
rlm@244 138 ray-reference-tips))
rlm@233 139 (vector
rlm@243 140 topology
rlm@233 141 (vec
rlm@243 142 (for [ray current-rays]
rlm@233 143 (do
rlm@233 144 (let [results (CollisionResults.)]
rlm@233 145 (.collideWith node ray results)
rlm@233 146 (let [touch-objects
rlm@233 147 (filter #(not (= geo (.getGeometry %)))
rlm@233 148 results)]
rlm@233 149 [(if (empty? touch-objects)
rlm@243 150 (.getLimit ray)
rlm@243 151 (.getDistance (first touch-objects)))
rlm@243 152 (.getLimit ray)])))))))))))
rlm@233 153
rlm@233 154 (defn touch!
rlm@233 155 "Endow the creature with the sense of touch. Returns a sequence of
rlm@233 156 functions, one for each body part with a tactile-sensor-proile,
rlm@233 157 each of which when called returns sensory data for that body part."
rlm@233 158 [#^Node creature]
rlm@233 159 (filter
rlm@233 160 (comp not nil?)
rlm@233 161 (map touch-kernel
rlm@233 162 (filter #(isa? (class %) Geometry)
rlm@233 163 (node-seq creature)))))
rlm@233 164 #+end_src
rlm@233 165
rlm@238 166 * Sensor Related Functions
rlm@238 167
rlm@238 168 These functions analyze the touch-sensor-profile image convert the
rlm@238 169 location of each touch sensor from pixel coordinates to UV-coordinates
rlm@238 170 and XYZ-coordinates.
rlm@238 171
rlm@238 172 #+name: sensors
rlm@238 173 #+begin_src clojure
rlm@240 174 (in-ns 'cortex.touch)
rlm@240 175
rlm@240 176 (defn feeler-pixel-coords
rlm@239 177 "Returns the coordinates of the feelers in pixel space in lists, one
rlm@239 178 list for each triangle, ordered in the same way as (triangles) and
rlm@239 179 (pixel-triangles)."
rlm@239 180 [#^Geometry geo image]
rlm@240 181 (map
rlm@240 182 (fn [pixel-triangle]
rlm@240 183 (filter
rlm@240 184 (fn [coord]
rlm@240 185 (inside-triangle? (->triangle pixel-triangle)
rlm@240 186 (->vector3f coord)))
rlm@240 187 (white-coordinates image (convex-bounds pixel-triangle))))
rlm@240 188 (pixel-triangles geo image)))
rlm@239 189
rlm@242 190 (defn feeler-world-coords [#^Geometry geo image]
rlm@240 191 (let [transforms
rlm@240 192 (map #(triangles->affine-transform
rlm@240 193 (->triangle %1) (->triangle %2))
rlm@240 194 (pixel-triangles geo image)
rlm@240 195 (triangles geo))]
rlm@242 196 (map (fn [transform coords]
rlm@240 197 (map #(.mult transform (->vector3f %)) coords))
rlm@240 198 transforms (feeler-pixel-coords geo image))))
rlm@239 199
rlm@242 200 (defn feeler-origins [#^Geometry geo image]
rlm@242 201 (reduce concat (feeler-world-coords geo image)))
rlm@242 202
rlm@240 203 (defn feeler-tips [#^Geometry geo image]
rlm@242 204 (let [world-coords (feeler-world-coords geo image)
rlm@241 205 normals
rlm@241 206 (map
rlm@241 207 (fn [triangle]
rlm@241 208 (.calculateNormal triangle)
rlm@241 209 (.clone (.getNormal triangle)))
rlm@241 210 (map ->triangle (triangles geo)))]
rlm@242 211
rlm@242 212 (mapcat (fn [origins normal]
rlm@242 213 (map #(.add % normal) origins))
rlm@242 214 world-coords normals)))
rlm@241 215
rlm@241 216 (defn touch-topology [#^Geometry geo image]
rlm@243 217 (collapse (reduce concat (feeler-pixel-coords geo image))))
rlm@238 218 #+end_src
rlm@238 219
rlm@233 220 * Visualizing Touch
rlm@233 221 #+name: visualization
rlm@233 222 #+begin_src clojure
rlm@233 223 (in-ns 'cortex.touch)
rlm@233 224
rlm@233 225 (defn touch->gray
rlm@245 226 "Convert a pair of [distance, max-distance] into a grayscale pixel."
rlm@233 227 [distance max-distance]
rlm@245 228 (gray (- 255 (rem (int (* 255 (/ distance max-distance))) 256))))
rlm@233 229
rlm@233 230 (defn view-touch
rlm@245 231 "Creates a function which accepts a list of touch sensor-data and
rlm@233 232 displays each element to the screen."
rlm@233 233 []
rlm@233 234 (view-sense
rlm@246 235 (fn [[coords sensor-data]]
rlm@233 236 (let [image (points->image coords)]
rlm@233 237 (dorun
rlm@233 238 (for [i (range (count coords))]
rlm@233 239 (.setRGB image ((coords i) 0) ((coords i) 1)
rlm@246 240 (apply touch->gray (sensor-data i))))) image))))
rlm@233 241 #+end_src
rlm@233 242
rlm@233 243
rlm@233 244
rlm@228 245 * Triangle Manipulation Functions
rlm@228 246
rlm@229 247 The rigid bodies which make up a creature have an underlying
rlm@229 248 =Geometry=, which is a =Mesh= plus a =Material= and other important
rlm@229 249 data involved with displaying the body.
rlm@229 250
rlm@229 251 A =Mesh= is composed of =Triangles=, and each =Triangle= has three
rlm@229 252 verticies which have coordinates in XYZ space and UV space.
rlm@229 253
rlm@229 254 Here, =(triangles)= gets all the triangles which compose a mesh, and
rlm@229 255 =(triangle-UV-coord)= returns the the UV coordinates of the verticies
rlm@229 256 of a triangle.
rlm@229 257
rlm@231 258 #+name: triangles-1
rlm@228 259 #+begin_src clojure
rlm@239 260 (in-ns 'cortex.touch)
rlm@239 261
rlm@239 262 (defn vector3f-seq [#^Vector3f v]
rlm@239 263 [(.getX v) (.getY v) (.getZ v)])
rlm@239 264
rlm@239 265 (defn triangle-seq [#^Triangle tri]
rlm@239 266 [(vector3f-seq (.get1 tri))
rlm@239 267 (vector3f-seq (.get2 tri))
rlm@239 268 (vector3f-seq (.get3 tri))])
rlm@239 269
rlm@240 270 (defn ->vector3f
rlm@240 271 ([coords] (Vector3f. (nth coords 0 0)
rlm@240 272 (nth coords 1 0)
rlm@240 273 (nth coords 2 0))))
rlm@239 274
rlm@239 275 (defn ->triangle [points]
rlm@239 276 (apply #(Triangle. %1 %2 %3) (map ->vector3f points)))
rlm@239 277
rlm@239 278 (defn triangle
rlm@245 279 "Get the triangle specified by triangle-index from the mesh."
rlm@239 280 [#^Geometry geo triangle-index]
rlm@239 281 (triangle-seq
rlm@239 282 (let [scratch (Triangle.)]
rlm@239 283 (.getTriangle (.getMesh geo) triangle-index scratch) scratch)))
rlm@239 284
rlm@228 285 (defn triangles
rlm@228 286 "Return a sequence of all the Triangles which compose a given
rlm@228 287 Geometry."
rlm@239 288 [#^Geometry geo]
rlm@239 289 (map (partial triangle geo) (range (.getTriangleCount (.getMesh geo)))))
rlm@228 290
rlm@228 291 (defn triangle-vertex-indices
rlm@228 292 "Get the triangle vertex indices of a given triangle from a given
rlm@228 293 mesh."
rlm@228 294 [#^Mesh mesh triangle-index]
rlm@228 295 (let [indices (int-array 3)]
rlm@228 296 (.getTriangle mesh triangle-index indices)
rlm@228 297 (vec indices)))
rlm@228 298
rlm@228 299 (defn vertex-UV-coord
rlm@228 300 "Get the UV-coordinates of the vertex named by vertex-index"
rlm@228 301 [#^Mesh mesh vertex-index]
rlm@228 302 (let [UV-buffer
rlm@228 303 (.getData
rlm@228 304 (.getBuffer
rlm@228 305 mesh
rlm@228 306 VertexBuffer$Type/TexCoord))]
rlm@228 307 [(.get UV-buffer (* vertex-index 2))
rlm@228 308 (.get UV-buffer (+ 1 (* vertex-index 2)))]))
rlm@228 309
rlm@239 310 (defn pixel-triangle [#^Geometry geo image index]
rlm@239 311 (let [mesh (.getMesh geo)
rlm@239 312 width (.getWidth image)
rlm@239 313 height (.getHeight image)]
rlm@239 314 (vec (map (fn [[u v]] (vector (* width u) (* height v)))
rlm@239 315 (map (partial vertex-UV-coord mesh)
rlm@239 316 (triangle-vertex-indices mesh index))))))
rlm@228 317
rlm@239 318 (defn pixel-triangles [#^Geometry geo image]
rlm@239 319 (let [height (.getHeight image)
rlm@239 320 width (.getWidth image)]
rlm@239 321 (map (partial pixel-triangle geo image)
rlm@239 322 (range (.getTriangleCount (.getMesh geo))))))
rlm@229 323
rlm@228 324 #+end_src
rlm@228 325
rlm@228 326 * Triangle Affine Transforms
rlm@228 327
rlm@229 328 The position of each hair is stored in a 2D image in UV
rlm@229 329 coordinates. To place the hair in 3D space we must convert from UV
rlm@229 330 coordinates to XYZ coordinates. Each =Triangle= has coordinates in
rlm@229 331 both UV-space and XYZ-space, which defines a unique [[http://mathworld.wolfram.com/AffineTransformation.html ][Affine Transform]]
rlm@229 332 for translating any coordinate within the UV triangle to the
rlm@229 333 cooresponding coordinate in the XYZ triangle.
rlm@229 334
rlm@231 335 #+name: triangles-3
rlm@228 336 #+begin_src clojure
rlm@243 337 (in-ns 'cortex.touch)
rlm@243 338
rlm@228 339 (defn triangle->matrix4f
rlm@228 340 "Converts the triangle into a 4x4 matrix: The first three columns
rlm@228 341 contain the vertices of the triangle; the last contains the unit
rlm@228 342 normal of the triangle. The bottom row is filled with 1s."
rlm@228 343 [#^Triangle t]
rlm@228 344 (let [mat (Matrix4f.)
rlm@228 345 [vert-1 vert-2 vert-3]
rlm@228 346 ((comp vec map) #(.get t %) (range 3))
rlm@228 347 unit-normal (do (.calculateNormal t)(.getNormal t))
rlm@228 348 vertices [vert-1 vert-2 vert-3 unit-normal]]
rlm@228 349 (dorun
rlm@228 350 (for [row (range 4) col (range 3)]
rlm@228 351 (do
rlm@228 352 (.set mat col row (.get (vertices row)col))
rlm@245 353 (.set mat 3 row 1)))) mat))
rlm@228 354
rlm@240 355 (defn triangles->affine-transform
rlm@228 356 "Returns the affine transformation that converts each vertex in the
rlm@228 357 first triangle into the corresponding vertex in the second
rlm@228 358 triangle."
rlm@228 359 [#^Triangle tri-1 #^Triangle tri-2]
rlm@228 360 (.mult
rlm@228 361 (triangle->matrix4f tri-2)
rlm@228 362 (.invert (triangle->matrix4f tri-1))))
rlm@228 363 #+end_src
rlm@228 364
rlm@239 365
rlm@239 366 * Schrapnel Conversion Functions
rlm@239 367
rlm@239 368 It is convienent to treat a =Triangle= as a sequence of verticies, and
rlm@239 369 a =Vector2f= and =Vector3f= as a sequence of floats. These conversion
rlm@239 370 functions make this easy. If these classes implemented =Iterable= then
rlm@239 371 this code would not be necessary. Hopefully they will in the future.
rlm@239 372
rlm@229 373 * Triangle Boundaries
rlm@229 374
rlm@229 375 For efficiency's sake I will divide the UV-image into small squares
rlm@229 376 which inscribe each UV-triangle, then extract the points which lie
rlm@229 377 inside the triangle and map them to 3D-space using
rlm@229 378 =(triangle-transform)= above. To do this I need a function,
rlm@229 379 =(inside-triangle?)=, which determines whether a point is inside a
rlm@229 380 triangle in 2D UV-space.
rlm@228 381
rlm@231 382 #+name: triangles-4
rlm@228 383 #+begin_src clojure
rlm@229 384 (defn convex-bounds
rlm@229 385 "Returns the smallest square containing the given vertices, as a
rlm@229 386 vector of integers [left top width height]."
rlm@240 387 [verts]
rlm@240 388 (let [xs (map first verts)
rlm@240 389 ys (map second verts)
rlm@229 390 x0 (Math/floor (apply min xs))
rlm@229 391 y0 (Math/floor (apply min ys))
rlm@229 392 x1 (Math/ceil (apply max xs))
rlm@229 393 y1 (Math/ceil (apply max ys))]
rlm@229 394 [x0 y0 (- x1 x0) (- y1 y0)]))
rlm@229 395
rlm@229 396 (defn same-side?
rlm@229 397 "Given the points p1 and p2 and the reference point ref, is point p
rlm@229 398 on the same side of the line that goes through p1 and p2 as ref is?"
rlm@229 399 [p1 p2 ref p]
rlm@229 400 (<=
rlm@229 401 0
rlm@229 402 (.dot
rlm@229 403 (.cross (.subtract p2 p1) (.subtract p p1))
rlm@229 404 (.cross (.subtract p2 p1) (.subtract ref p1)))))
rlm@229 405
rlm@229 406 (defn inside-triangle?
rlm@229 407 "Is the point inside the triangle?"
rlm@229 408 {:author "Dylan Holmes"}
rlm@229 409 [#^Triangle tri #^Vector3f p]
rlm@240 410 (let [[vert-1 vert-2 vert-3] [(.get1 tri) (.get2 tri) (.get3 tri)]]
rlm@229 411 (and
rlm@229 412 (same-side? vert-1 vert-2 vert-3 p)
rlm@229 413 (same-side? vert-2 vert-3 vert-1 p)
rlm@229 414 (same-side? vert-3 vert-1 vert-2 p))))
rlm@229 415 #+end_src
rlm@229 416
rlm@228 417 * Physics Collision Objects
rlm@230 418
rlm@234 419 The "hairs" are actually =Rays= which extend from a point on a
rlm@230 420 =Triangle= in the =Mesh= normal to the =Triangle's= surface.
rlm@230 421
rlm@226 422 * Headers
rlm@231 423
rlm@231 424 #+name: touch-header
rlm@226 425 #+begin_src clojure
rlm@226 426 (ns cortex.touch
rlm@226 427 "Simulate the sense of touch in jMonkeyEngine3. Enables any Geometry
rlm@226 428 to be outfitted with touch sensors with density determined by a UV
rlm@226 429 image. In this way a Geometry can know what parts of itself are
rlm@226 430 touching nearby objects. Reads specially prepared blender files to
rlm@226 431 construct this sense automatically."
rlm@226 432 {:author "Robert McIntyre"}
rlm@226 433 (:use (cortex world util sense))
rlm@226 434 (:use clojure.contrib.def)
rlm@226 435 (:import (com.jme3.scene Geometry Node Mesh))
rlm@226 436 (:import com.jme3.collision.CollisionResults)
rlm@226 437 (:import com.jme3.scene.VertexBuffer$Type)
rlm@226 438 (:import (com.jme3.math Triangle Vector3f Vector2f Ray Matrix4f)))
rlm@226 439 #+end_src
rlm@37 440
rlm@232 441 * Adding Touch to the Worm
rlm@232 442
rlm@232 443 #+name: test-touch
rlm@232 444 #+begin_src clojure
rlm@232 445 (ns cortex.test.touch
rlm@232 446 (:use (cortex world util sense body touch))
rlm@232 447 (:use cortex.test.body))
rlm@232 448
rlm@232 449 (cortex.import/mega-import-jme3)
rlm@232 450
rlm@232 451 (defn test-touch []
rlm@232 452 (let [the-worm (doto (worm) (body!))
rlm@232 453 touch (touch! the-worm)
rlm@232 454 touch-display (view-touch)]
rlm@232 455 (world (nodify [the-worm (floor)])
rlm@232 456 standard-debug-controls
rlm@232 457
rlm@232 458 (fn [world]
rlm@244 459 (speed-up world)
rlm@232 460 (light-up-everything world))
rlm@232 461
rlm@232 462 (fn [world tpf]
rlm@246 463 (touch-display
rlm@246 464 (map #(% (.getRootNode world)) touch))))))
rlm@232 465 #+end_src
rlm@228 466 * Source Listing
rlm@228 467 * Next
rlm@228 468
rlm@228 469
rlm@226 470 * COMMENT Code Generation
rlm@39 471 #+begin_src clojure :tangle ../src/cortex/touch.clj
rlm@231 472 <<touch-header>>
rlm@231 473 <<meta-data>>
rlm@231 474 <<triangles-1>>
rlm@231 475 <<triangles-3>>
rlm@231 476 <<triangles-4>>
rlm@231 477 <<sensors>>
rlm@231 478 <<kernel>>
rlm@231 479 <<visualization>>
rlm@0 480 #+end_src
rlm@0 481
rlm@232 482
rlm@68 483 #+begin_src clojure :tangle ../src/cortex/test/touch.clj
rlm@232 484 <<test-touch>>
rlm@39 485 #+end_src
rlm@39 486
rlm@0 487
rlm@0 488
rlm@0 489
rlm@32 490
rlm@32 491
rlm@226 492