annotate org/touch.org @ 187:6142e85f5825

extracted common elements of display code
author Robert McIntyre <rlm@mit.edu>
date Sat, 04 Feb 2012 09:42:19 -0700
parents cfb71209ddc6
children 22548d48cc85
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@37 11 My creatures need to be able to feel their environments. The idea here
rlm@43 12 is to create thousands of small /touch receptors/ along the geometries
rlm@43 13 which make up the creature's body. The number of touch receptors in a
rlm@43 14 given area is determined by how complicated that area is, as
rlm@43 15 determined by the total number of triangles in that region. This way,
rlm@43 16 complicated regions like the hands/face, etc. get more touch receptors
rlm@43 17 than simpler areas of the body.
rlm@0 18
rlm@66 19 #+name: skin-main
rlm@0 20 #+begin_src clojure
rlm@37 21 (ns cortex.touch
rlm@37 22 "Simulate the sense of touch in jMonkeyEngine3. Enables any Geometry
rlm@37 23 to be outfitted with touch sensors with density proportional to the
rlm@37 24 density of triangles along the surface of the Geometry. Enables a
rlm@37 25 Geometry to know what parts of itself are touching nearby objects."
rlm@37 26 {:author "Robert McIntyre"}
rlm@156 27 (:use (cortex world util sense))
rlm@178 28 (:use clojure.contrib.def)
rlm@179 29 (:import (com.jme3.scene Geometry Node Mesh))
rlm@39 30 (:import com.jme3.collision.CollisionResults)
rlm@179 31 (:import com.jme3.scene.VertexBuffer$Type)
rlm@179 32 (:import (com.jme3.math Triangle Vector3f Vector2f Ray Matrix4f)))
rlm@37 33
rlm@37 34 (defn triangles
rlm@37 35 "Return a sequence of all the Triangles which compose a given
rlm@37 36 Geometry."
rlm@37 37 [#^Geometry geom]
rlm@6 38 (let
rlm@6 39 [mesh (.getMesh geom)
rlm@6 40 triangles (transient [])]
rlm@6 41 (dorun
rlm@6 42 (for [n (range (.getTriangleCount mesh))]
rlm@6 43 (let [tri (Triangle.)]
rlm@6 44 (.getTriangle mesh n tri)
rlm@37 45 ;; (.calculateNormal tri)
rlm@37 46 ;; (.calculateCenter tri)
rlm@6 47 (conj! triangles tri))))
rlm@6 48 (persistent! triangles)))
rlm@6 49
rlm@7 50 (defn get-ray-origin
rlm@37 51 "Return the origin which a Ray would have to have to be in the exact
rlm@37 52 center of a particular Triangle in the Geometry in World
rlm@37 53 Coordinates."
rlm@7 54 [geom tri]
rlm@7 55 (let [new (Vector3f.)]
rlm@7 56 (.calculateCenter tri)
rlm@37 57 (.localToWorld geom (.getCenter tri) new) new))
rlm@6 58
rlm@7 59 (defn get-ray-direction
rlm@156 60 "Return the direction which a Ray would have to have to be to point
rlm@37 61 normal to the Triangle, in coordinates relative to the center of the
rlm@37 62 Triangle."
rlm@7 63 [geom tri]
rlm@9 64 (let [n+c (Vector3f.)]
rlm@7 65 (.calculateNormal tri)
rlm@9 66 (.calculateCenter tri)
rlm@37 67 (.localToWorld
rlm@37 68 geom
rlm@37 69 (.add (.getCenter tri) (.getNormal tri)) n+c)
rlm@37 70 (.subtract n+c (get-ray-origin geom tri))))
rlm@37 71
rlm@156 72 ;; Every Mesh has many triangles, each with its own index.
rlm@156 73 ;; Every vertex has its own index as well.
rlm@37 74
rlm@178 75 (defn tactile-sensor-profile
rlm@156 76 "Return the touch-sensor distribution image in BufferedImage format,
rlm@156 77 or nil if it does not exist."
rlm@156 78 [#^Geometry obj]
rlm@156 79 (if-let [image-path (meta-data obj "touch")]
rlm@178 80 (load-image image-path)))
rlm@156 81
rlm@156 82 (defn triangle
rlm@156 83 "Get the triangle specified by triangle-index from the mesh within
rlm@156 84 bounds."
rlm@156 85 [#^Mesh mesh triangle-index]
rlm@156 86 (let [scratch (Triangle.)]
rlm@156 87 (.getTriangle mesh triangle-index scratch)
rlm@156 88 scratch))
rlm@156 89
rlm@156 90 (defn triangle-vertex-indices
rlm@156 91 "Get the triangle vertex indices of a given triangle from a given
rlm@156 92 mesh."
rlm@156 93 [#^Mesh mesh triangle-index]
rlm@156 94 (let [indices (int-array 3)]
rlm@156 95 (.getTriangle mesh triangle-index indices)
rlm@156 96 (vec indices)))
rlm@156 97
rlm@156 98 (defn vertex-UV-coord
rlm@156 99 "Get the uv-coordinates of the vertex named by vertex-index"
rlm@156 100 [#^Mesh mesh vertex-index]
rlm@156 101 (let [UV-buffer
rlm@156 102 (.getData
rlm@156 103 (.getBuffer
rlm@156 104 mesh
rlm@156 105 VertexBuffer$Type/TexCoord))]
rlm@156 106 [(.get UV-buffer (* vertex-index 2))
rlm@156 107 (.get UV-buffer (+ 1 (* vertex-index 2)))]))
rlm@156 108
rlm@156 109 (defn triangle-UV-coord
rlm@156 110 "Get the uv-cooridnates of the triangle's verticies."
rlm@156 111 [#^Mesh mesh width height triangle-index]
rlm@156 112 (map (fn [[u v]] (vector (* width u) (* height v)))
rlm@156 113 (map (partial vertex-UV-coord mesh)
rlm@156 114 (triangle-vertex-indices mesh triangle-index))))
rlm@156 115
rlm@156 116 (defn same-side?
rlm@156 117 "Given the points p1 and p2 and the reference point ref, is point p
rlm@156 118 on the same side of the line that goes through p1 and p2 as ref is?"
rlm@156 119 [p1 p2 ref p]
rlm@156 120 (<=
rlm@156 121 0
rlm@156 122 (.dot
rlm@156 123 (.cross (.subtract p2 p1) (.subtract p p1))
rlm@156 124 (.cross (.subtract p2 p1) (.subtract ref p1)))))
rlm@156 125
rlm@156 126 (defn triangle-seq [#^Triangle tri]
rlm@156 127 [(.get1 tri) (.get2 tri) (.get3 tri)])
rlm@156 128
rlm@156 129 (defn vector3f-seq [#^Vector3f v]
rlm@156 130 [(.getX v) (.getY v) (.getZ v)])
rlm@156 131
rlm@156 132 (defn inside-triangle?
rlm@156 133 "Is the point inside the triangle?"
rlm@156 134 {:author "Dylan Holmes"}
rlm@156 135 [#^Triangle tri #^Vector3f p]
rlm@156 136 (let [[vert-1 vert-2 vert-3] (triangle-seq tri)]
rlm@156 137 (and
rlm@156 138 (same-side? vert-1 vert-2 vert-3 p)
rlm@156 139 (same-side? vert-2 vert-3 vert-1 p)
rlm@156 140 (same-side? vert-3 vert-1 vert-2 p))))
rlm@156 141
rlm@156 142 (defn triangle->matrix4f
rlm@156 143 "Converts the triangle into a 4x4 matrix: The first three columns
rlm@156 144 contain the vertices of the triangle; the last contains the unit
rlm@156 145 normal of the triangle. The bottom row is filled with 1s."
rlm@156 146 [#^Triangle t]
rlm@156 147 (let [mat (Matrix4f.)
rlm@156 148 [vert-1 vert-2 vert-3]
rlm@156 149 ((comp vec map) #(.get t %) (range 3))
rlm@156 150 unit-normal (do (.calculateNormal t)(.getNormal t))
rlm@156 151 vertices [vert-1 vert-2 vert-3 unit-normal]]
rlm@156 152 (dorun
rlm@156 153 (for [row (range 4) col (range 3)]
rlm@37 154 (do
rlm@156 155 (.set mat col row (.get (vertices row)col))
rlm@156 156 (.set mat 3 row 1))))
rlm@156 157 mat))
rlm@156 158
rlm@156 159 (defn triangle-transformation
rlm@156 160 "Returns the affine transformation that converts each vertex in the
rlm@156 161 first triangle into the corresponding vertex in the second
rlm@156 162 triangle."
rlm@156 163 [#^Triangle tri-1 #^Triangle tri-2]
rlm@156 164 (.mult
rlm@156 165 (triangle->matrix4f tri-2)
rlm@156 166 (.invert (triangle->matrix4f tri-1))))
rlm@156 167
rlm@156 168 (defn point->vector2f [[u v]]
rlm@156 169 (Vector2f. u v))
rlm@156 170
rlm@156 171 (defn vector2f->vector3f [v]
rlm@156 172 (Vector3f. (.getX v) (.getY v) 0))
rlm@156 173
rlm@156 174 (defn map-triangle [f #^Triangle tri]
rlm@156 175 (Triangle.
rlm@156 176 (f 0 (.get1 tri))
rlm@156 177 (f 1 (.get2 tri))
rlm@156 178 (f 2 (.get3 tri))))
rlm@156 179
rlm@156 180 (defn points->triangle
rlm@156 181 "Convert a list of points into a triangle."
rlm@156 182 [points]
rlm@156 183 (apply #(Triangle. %1 %2 %3)
rlm@156 184 (map (fn [point]
rlm@156 185 (let [point (vec point)]
rlm@156 186 (Vector3f. (get point 0 0)
rlm@156 187 (get point 1 0)
rlm@156 188 (get point 2 0))))
rlm@156 189 (take 3 points))))
rlm@156 190
rlm@156 191 (defn convex-bounds
rlm@178 192 "Returns the smallest square containing the given vertices, as a
rlm@178 193 vector of integers [left top width height]."
rlm@156 194 [uv-verts]
rlm@156 195 (let [xs (map first uv-verts)
rlm@156 196 ys (map second uv-verts)
rlm@156 197 x0 (Math/floor (apply min xs))
rlm@156 198 y0 (Math/floor (apply min ys))
rlm@156 199 x1 (Math/ceil (apply max xs))
rlm@156 200 y1 (Math/ceil (apply max ys))]
rlm@156 201 [x0 y0 (- x1 x0) (- y1 y0)]))
rlm@156 202
rlm@156 203 (defn sensors-in-triangle
rlm@178 204 "Locate the touch sensors in the triangle, returning a map of their
rlm@178 205 UV and geometry-relative coordinates."
rlm@156 206 [image mesh tri-index]
rlm@156 207 (let [width (.getWidth image)
rlm@156 208 height (.getHeight image)
rlm@156 209 UV-vertex-coords (triangle-UV-coord mesh width height tri-index)
rlm@156 210 bounds (convex-bounds UV-vertex-coords)
rlm@156 211
rlm@156 212 cutout-triangle (points->triangle UV-vertex-coords)
rlm@156 213 UV-sensor-coords
rlm@156 214 (filter (comp (partial inside-triangle? cutout-triangle)
rlm@156 215 (fn [[u v]] (Vector3f. u v 0)))
rlm@156 216 (white-coordinates image bounds))
rlm@156 217 UV->geometry (triangle-transformation
rlm@156 218 cutout-triangle
rlm@156 219 (triangle mesh tri-index))
rlm@156 220 geometry-sensor-coords
rlm@156 221 (map (fn [[u v]] (.mult UV->geometry (Vector3f. u v 0)))
rlm@156 222 UV-sensor-coords)]
rlm@156 223 {:UV UV-sensor-coords :geometry geometry-sensor-coords}))
rlm@156 224
rlm@156 225 (defn-memo locate-feelers
rlm@178 226 "Search the geometry's tactile UV profile for touch sensors,
rlm@178 227 returning their positions in geometry-relative coordinates."
rlm@156 228 [#^Geometry geo]
rlm@156 229 (let [mesh (.getMesh geo)
rlm@156 230 num-triangles (.getTriangleCount mesh)]
rlm@178 231 (if-let [image (tactile-sensor-profile geo)]
rlm@156 232 (map
rlm@156 233 (partial sensors-in-triangle image mesh)
rlm@156 234 (range num-triangles))
rlm@156 235 (repeat (.getTriangleCount mesh) {:UV nil :geometry nil}))))
rlm@156 236
rlm@178 237 (defn-memo touch-topology
rlm@178 238 "Return a sequence of vectors of the form [x y] describing the
rlm@178 239 \"topology\" of the tactile sensors. Points that are close together
rlm@178 240 in the touch-topology are generally close together in the simulation."
rlm@178 241 [#^Gemoetry geo]
rlm@156 242 (vec (collapse (reduce concat (map :UV (locate-feelers geo))))))
rlm@156 243
rlm@178 244 (defn-memo feeler-coordinates
rlm@178 245 "The location of the touch sensors in world-space coordinates."
rlm@178 246 [#^Geometry geo]
rlm@156 247 (vec (map :geometry (locate-feelers geo))))
rlm@156 248
rlm@178 249 (defn touch-fn
rlm@178 250 "Returns a function which returns tactile sensory data when called
rlm@178 251 inside a running simulation."
rlm@178 252 [#^Geometry geo]
rlm@156 253 (let [feeler-coords (feeler-coordinates geo)
rlm@156 254 tris (triangles geo)
rlm@156 255 limit 0.1
rlm@156 256 ;;results (CollisionResults.)
rlm@156 257 ]
rlm@156 258 (if (empty? (touch-topology geo))
rlm@156 259 nil
rlm@156 260 (fn [node]
rlm@156 261 (let [sensor-origins
rlm@156 262 (map
rlm@156 263 #(map (partial local-to-world geo) %)
rlm@156 264 feeler-coords)
rlm@156 265 triangle-normals
rlm@156 266 (map (partial get-ray-direction geo)
rlm@156 267 tris)
rlm@156 268 rays
rlm@156 269 (flatten
rlm@156 270 (map (fn [origins norm]
rlm@156 271 (map #(doto (Ray. % norm)
rlm@156 272 (.setLimit limit)) origins))
rlm@156 273 sensor-origins triangle-normals))]
rlm@156 274 (vector
rlm@156 275 (touch-topology geo)
rlm@156 276 (vec
rlm@156 277 (for [ray rays]
rlm@156 278 (do
rlm@156 279 (let [results (CollisionResults.)]
rlm@156 280 (.collideWith node ray results)
rlm@156 281 (let [touch-objects
rlm@156 282 (filter #(not (= geo (.getGeometry %)))
rlm@156 283 results)]
rlm@156 284 (- 255
rlm@156 285 (if (empty? touch-objects) 255
rlm@156 286 (rem
rlm@156 287 (int
rlm@156 288 (* 255 (/ (.getDistance
rlm@156 289 (first touch-objects)) limit)))
rlm@156 290 256))))))))))))))
rlm@156 291
rlm@178 292 (defn touch!
rlm@178 293 "Endow the creature with the sense of touch. Returns a sequence of
rlm@178 294 functions, one for each body part with a tactile-sensor-proile,
rlm@178 295 each of which when called returns sensory data for that body part."
rlm@178 296 [#^Node creature]
rlm@178 297 (filter
rlm@178 298 (comp not nil?)
rlm@178 299 (map touch-fn
rlm@178 300 (filter #(isa? (class %) Geometry)
rlm@178 301 (node-seq creature)))))
rlm@156 302
rlm@187 303 (defn gray
rlm@185 304 "Create a gray RGB pixel with R, G, and B set to 'num"
rlm@185 305 [num]
rlm@185 306 (+ num
rlm@185 307 (bit-shift-left num 8)
rlm@185 308 (bit-shift-left num 16)))
rlm@185 309
rlm@187 310 (defvar
rlm@187 311 view-touch
rlm@187 312 (view-sense
rlm@187 313 (fn
rlm@187 314 [[coords sensor-data]]
rlm@187 315 (let [image (points->image coords)]
rlm@187 316 (dorun
rlm@187 317 (for [i (range (count coords))]
rlm@187 318 (.setRGB image ((coords i) 0) ((coords i) 1)
rlm@187 319 (gray (sensor-data i)))))
rlm@187 320 image)))
rlm@185 321 "Creates a function which accepts touch sensor-data and displays it
rlm@187 322 as BufferedImages in JFrames.")
rlm@187 323
rlm@156 324
rlm@37 325 #+end_src
rlm@37 326
rlm@37 327
rlm@37 328 * Example
rlm@37 329
rlm@66 330 #+name: touch-test
rlm@37 331 #+begin_src clojure
rlm@68 332 (ns cortex.test.touch
rlm@59 333 (:use (cortex world util touch))
rlm@59 334 (:import
rlm@59 335 com.jme3.scene.shape.Sphere
rlm@59 336 com.jme3.math.ColorRGBA
rlm@59 337 com.jme3.math.Vector3f
rlm@59 338 com.jme3.material.RenderState$BlendMode
rlm@59 339 com.jme3.renderer.queue.RenderQueue$Bucket
rlm@59 340 com.jme3.scene.shape.Box
rlm@68 341 com.jme3.scene.Node))
rlm@39 342
rlm@7 343 (defn ray-origin-debug
rlm@9 344 [ray color]
rlm@7 345 (make-shape
rlm@20 346 (assoc base-shape
rlm@20 347 :shape (Sphere. 5 5 0.05)
rlm@20 348 :name "arrow"
rlm@20 349 :color color
rlm@20 350 :texture false
rlm@20 351 :physical? false
rlm@20 352 :position
rlm@20 353 (.getOrigin ray))))
rlm@6 354
rlm@9 355 (defn ray-debug [ray color]
rlm@6 356 (make-shape
rlm@6 357 (assoc
rlm@6 358 base-shape
rlm@6 359 :name "debug-ray"
rlm@6 360 :physical? false
rlm@6 361 :shape (com.jme3.scene.shape.Line.
rlm@6 362 (.getOrigin ray)
rlm@6 363 (.add
rlm@6 364 (.getOrigin ray)
rlm@6 365 (.mult (.getDirection ray)
rlm@6 366 (float (.getLimit ray))))))))
rlm@6 367
rlm@6 368
rlm@10 369 (defn contact-color [contacts]
rlm@10 370 (case contacts
rlm@10 371 0 ColorRGBA/Gray
rlm@37 372 1 ColorRGBA/Red
rlm@10 373 2 ColorRGBA/Green
rlm@10 374 3 ColorRGBA/Yellow
rlm@10 375 4 ColorRGBA/Orange
rlm@10 376 5 ColorRGBA/Red
rlm@10 377 6 ColorRGBA/Magenta
rlm@10 378 7 ColorRGBA/Pink
rlm@10 379 8 ColorRGBA/White))
rlm@6 380
rlm@14 381 (defn update-ray-debug [node ray contacts]
rlm@14 382 (let [origin (.getChild node 0)]
rlm@14 383 (.setLocalTranslation origin (.getOrigin ray))
rlm@14 384 (.setColor (.getMaterial origin) "Color" (contact-color contacts))))
rlm@14 385
rlm@13 386 (defn init-node
rlm@13 387 [debug-node rays]
rlm@12 388 (.detachAllChildren debug-node)
rlm@13 389 (dorun
rlm@13 390 (for [ray rays]
rlm@13 391 (do
rlm@13 392 (.attachChild
rlm@13 393 debug-node
rlm@13 394 (doto (Node.)
rlm@14 395 (.attachChild (ray-origin-debug ray ColorRGBA/Gray))
rlm@20 396 (.attachChild (ray-debug ray ColorRGBA/Gray))
rlm@14 397 ))))))
rlm@14 398
rlm@13 399 (defn manage-ray-debug-node [debug-node geom touch-data limit]
rlm@13 400 (let [rays (normal-rays limit geom)]
rlm@13 401 (if (not= (count (.getChildren debug-node)) (count touch-data))
rlm@13 402 (init-node debug-node rays))
rlm@13 403 (dorun
rlm@13 404 (for [n (range (count touch-data))]
rlm@14 405 (update-ray-debug
rlm@14 406 (.getChild debug-node n) (nth rays n) (nth touch-data n))))))
rlm@12 407
rlm@0 408 (defn transparent-sphere []
rlm@0 409 (doto
rlm@0 410 (make-shape
rlm@0 411 (merge base-shape
rlm@0 412 {:position (Vector3f. 0 2 0)
rlm@0 413 :name "the blob."
rlm@0 414 :material "Common/MatDefs/Misc/Unshaded.j3md"
rlm@0 415 :texture "Textures/purpleWisp.png"
rlm@0 416 :physical? true
rlm@0 417 :mass 70
rlm@0 418 :color ColorRGBA/Blue
rlm@0 419 :shape (Sphere. 10 10 1)}))
rlm@0 420 (-> (.getMaterial)
rlm@0 421 (.getAdditionalRenderState)
rlm@0 422 (.setBlendMode RenderState$BlendMode/Alpha))
rlm@0 423 (.setQueueBucket RenderQueue$Bucket/Transparent)))
rlm@0 424
rlm@0 425 (defn transparent-box []
rlm@0 426 (doto
rlm@0 427 (make-shape
rlm@0 428 (merge base-shape
rlm@0 429 {:position (Vector3f. 0 2 0)
rlm@10 430 :name "box"
rlm@0 431 :material "Common/MatDefs/Misc/Unshaded.j3md"
rlm@0 432 :texture "Textures/purpleWisp.png"
rlm@0 433 :physical? true
rlm@0 434 :mass 70
rlm@0 435 :color ColorRGBA/Blue
rlm@0 436 :shape (Box. 1 1 1)}))
rlm@0 437 (-> (.getMaterial)
rlm@0 438 (.getAdditionalRenderState)
rlm@0 439 (.setBlendMode RenderState$BlendMode/Alpha))
rlm@0 440 (.setQueueBucket RenderQueue$Bucket/Transparent)))
rlm@0 441
rlm@6 442 (defn transparent-floor []
rlm@6 443 (doto
rlm@6 444 (box 5 0.2 5 :mass 0 :position (Vector3f. 0 -2 0)
rlm@6 445 :material "Common/MatDefs/Misc/Unshaded.j3md"
rlm@10 446 :texture "Textures/redWisp.png"
rlm@10 447 :name "floor")
rlm@6 448 (-> (.getMaterial)
rlm@6 449 (.getAdditionalRenderState)
rlm@6 450 (.setBlendMode RenderState$BlendMode/Alpha))
rlm@6 451 (.setQueueBucket RenderQueue$Bucket/Transparent)))
rlm@6 452
rlm@69 453 (defn test-skin
rlm@69 454 "Testing touch:
rlm@69 455 you should see a ball which responds to the table
rlm@69 456 and whatever balls hit it."
rlm@69 457 []
rlm@0 458 (let [b
rlm@58 459 ;;(transparent-box)
rlm@58 460 (transparent-sphere)
rlm@10 461 ;;(sphere)
rlm@58 462 f (transparent-floor)
rlm@6 463 debug-node (Node.)
rlm@12 464 node (doto (Node.) (.attachChild b) (.attachChild f))
rlm@12 465 root-node (doto (Node.) (.attachChild node)
rlm@12 466 (.attachChild debug-node))
rlm@12 467 ]
rlm@0 468
rlm@0 469 (world
rlm@12 470 root-node
rlm@15 471 {"key-return" (fire-cannon-ball node)}
rlm@0 472 (fn [world]
rlm@20 473 ;; (Capture/SimpleCaptureVideo
rlm@20 474 ;; world
rlm@20 475 ;; (file-str "/home/r/proj/cortex/tmp/blob.avi"))
rlm@20 476 ;; (no-logging)
rlm@20 477 ;;(enable-debug world)
rlm@20 478 ;; (set-accuracy world (/ 1 60))
rlm@58 479 )
rlm@58 480
rlm@0 481 (fn [& _]
rlm@19 482 (let [sensitivity 0.2
rlm@18 483 touch-data (touch-percieve sensitivity b node)]
rlm@68 484 (manage-ray-debug-node debug-node b touch-data sensitivity))
rlm@0 485 ))))
rlm@0 486
rlm@37 487
rlm@0 488 #+end_src
rlm@0 489
rlm@0 490
rlm@10 491
rlm@10 492
rlm@6 493
rlm@0 494 * COMMENT code generation
rlm@39 495 #+begin_src clojure :tangle ../src/cortex/touch.clj
rlm@0 496 <<skin-main>>
rlm@0 497 #+end_src
rlm@0 498
rlm@68 499 #+begin_src clojure :tangle ../src/cortex/test/touch.clj
rlm@39 500 <<touch-test>>
rlm@39 501 #+end_src
rlm@39 502
rlm@0 503
rlm@0 504
rlm@0 505
rlm@32 506
rlm@32 507