├── .gitattributes
├── .gitignore
├── DRAFT-cljs-gravity.pdf
├── README.md
├── project.clj
├── resources
└── public
│ ├── assets
│ └── img
│ │ ├── ball.png
│ │ ├── ball.xcf
│ │ ├── circle.png
│ │ └── circle.xcf
│ ├── css
│ └── style.css
│ ├── force-worker
│ └── worker.js
│ ├── index.html
│ └── libs
│ ├── OrbitControls.js
│ ├── d3-3d.js
│ ├── d3.js
│ ├── d3.layout.force3d.js
│ ├── stats.min.js
│ └── three.js
└── src
└── gravity
├── events.cljs
├── force
├── proxy.cljs
└── worker.cljs
├── graph.cljs
├── tools.cljs
└── view
├── events_generator.cljs
├── graph.cljs
├── graph_tools.cljs
├── node.cljs
└── nodeset.cljs
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | *jar
3 | /lib/
4 | /classes/
5 | /out/
6 | /release/
7 | /target/
8 | /repl/
9 | /.repl*/
10 | /temp/
11 | /scripts/
12 | .lein-deps-sum
13 | .lein-repl-history
14 | .lein-plugins/
15 |
16 | .nrepl*
17 |
18 | /resources/public/js/compiled/**
19 | figwheel_server.log
20 | pom.xml
21 | *jar
22 | /lib/
23 | /classes/
24 | /out/
25 | /target/
26 | .lein-deps-sum
27 | .lein-repl-history
28 | .lein-plugins/
29 | .repl
30 | .nrepl-port
31 | project.sublime-workspace
32 | project.sublime-project
--------------------------------------------------------------------------------
/DRAFT-cljs-gravity.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/DRAFT-cljs-gravity.pdf
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## A 3D force layout using D3.js and THREE.js
2 |
3 | Cljs-gravity (please help me to find a better name!) is a ClojureScript library that plot an interactive graph, animated by a Barnes-Hut simulation.
4 |
5 | **This is a work in progress**, see the demo: http://ggeoffrey.github.io/ (Chrome or Chromium recomended ATM)
6 |
7 | The goal is to make a safe and stable 3D graph visualisation that:
8 | - do one thing and do it well,
9 | - rely on quasi-standard tools,
10 | - ensure there is no side effects,
11 | - ensure there is no memory leaks,
12 | - use webworkers in an easy, safe and elegant way,
13 | - provide a rich set of events.
14 |
15 | ### Report
16 | See [this document](https://github.com/ggeoffrey/cljs-gravity/raw/master/DRAFT-cljs-gravity.pdf)
17 | for full knowledge on the library's content and rationals.
18 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject gravity "0.1.0-SNAPSHOT"
2 | :description "FIXME: write this!"
3 | :url "http://example.com/FIXME"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 |
7 | :dependencies [[org.clojure/clojure "1.7.0"]
8 | [org.clojure/clojurescript "1.7.122"]
9 | [org.clojure/core.async "0.1.346.0-17112a-alpha"]]
10 |
11 | :plugins [[lein-cljsbuild "1.1.0"]
12 | [lein-figwheel "0.4.0"]]
13 |
14 | :source-paths ["src"]
15 |
16 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
17 |
18 | :cljsbuild {
19 | :builds [{:id "dev"
20 | :source-paths ["src"]
21 |
22 | :figwheel { :on-jsload "gravity.graph/on-js-reload" }
23 |
24 | :compiler {:main gravity.graph
25 | :asset-path "js/compiled/out"
26 | :output-to "resources/public/js/compiled/gravity.js"
27 | :output-dir "resources/public/js/compiled/out"
28 | :source-map-timestamp true }}
29 | {:id "min"
30 | :source-paths ["src"]
31 | :compiler {:output-to "resources/public/js/compiled/gravity.js"
32 | :main gravity.graph
33 | :optimizations :advanced
34 | :pretty-print false
35 | :externs ["resources/public/libs/three.js"
36 | "resources/public/libs/OrbitControls.js"
37 | "resources/public/libs/d3-3d.js"
38 | "resources/public/libs/stats.min.js"]
39 | :closure-warnings {:externs-validation :off}}}]}
40 |
41 | :figwheel {
42 | ;; :http-server-root "public" ;; default and assumes "resources"
43 | ;; :server-port 3449 ;; default
44 | ;; :server-ip "127.0.0.1"
45 |
46 | :css-dirs ["resources/public/css"] ;; watch and update CSS
47 |
48 | ;; Start an nREPL server into the running figwheel process
49 | ;; :nrepl-port 7888
50 |
51 | ;; Server Ring Handler (optional)
52 | ;; if you want to embed a ring handler into the figwheel http-kit
53 | ;; server, this is for simple ring servers, if this
54 | ;; doesn't work for you just run your own server :)
55 | ;; :ring-handler hello_world.server/handler
56 |
57 | ;; To be able to open files in your editor from the heads up display
58 | ;; you will need to put a script on your path.
59 | ;; that script will have to take a file path and a line number
60 | ;; ie. in ~/bin/myfile-opener
61 | ;; #! /bin/sh
62 | ;; emacsclient -n +$2 $1
63 | ;;
64 | ;; :open-file-command "myfile-opener"
65 |
66 | ;; if you want to disable the REPL
67 | ;; :repl false
68 |
69 | ;; to configure a different figwheel logfile path
70 | ;; :server-logfile "tmp/logs/figwheel-logfile.log"
71 | })
72 |
--------------------------------------------------------------------------------
/resources/public/assets/img/ball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/ball.png
--------------------------------------------------------------------------------
/resources/public/assets/img/ball.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/ball.xcf
--------------------------------------------------------------------------------
/resources/public/assets/img/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/circle.png
--------------------------------------------------------------------------------
/resources/public/assets/img/circle.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ggeoffrey/cljs-gravity/21f15aa08e88d747bc6dbe356a31c7d4d35d0bb9/resources/public/assets/img/circle.xcf
--------------------------------------------------------------------------------
/resources/public/css/style.css:
--------------------------------------------------------------------------------
1 | /* some style */
2 |
3 | html, body{
4 | padding: 0;
5 | margin: 0;
6 | }
7 |
8 | #container {
9 | width : 50%;
10 | height : auto;
11 | margin : 0em;
12 | }
13 |
14 | #target{
15 | width : 100%;
16 | height: 100%;
17 | }
18 |
19 | #tooltip{
20 | position: absolute;
21 | padding: 1em;
22 | background-color: #B0B0B0;
23 | opacity: 0.95;
24 | font-weight: bold;
25 |
26 | margin: 1em;
27 | display: none;
28 | }
29 |
--------------------------------------------------------------------------------
/resources/public/force-worker/worker.js:
--------------------------------------------------------------------------------
1 | // BOOTSTRAP
2 |
3 | //*
4 | importScripts( "../js/compiled/out/goog/base.js",
5 | "../js/compiled/out/goog/deps.js",
6 | "../js/compiled/out/goog/object/object.js",
7 | "../js/compiled/out/cljs_deps.js",
8 | "../js/compiled/out/cljs/core.js",
9 | "../js/compiled/out/gravity/force/worker.js"
10 | );
11 |
12 |
13 | importScripts("../libs/d3-3d.js");
14 |
15 |
16 | goog.require("gravity.force.worker");
17 | gravity.force.worker.create();
18 | //*/
19 |
20 | /*
21 | importScripts("../out/gravity.js");
22 |
23 | goog.require("gravity.force.worker");
24 | gravity.force.worker.main();
25 |
26 | //*/
27 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/resources/public/libs/OrbitControls.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author qiao / https://github.com/qiao
3 | * @author mrdoob / http://mrdoob.com
4 | * @author alteredq / http://alteredqualia.com/
5 | * @author WestLangley / http://github.com/WestLangley
6 | * @author erich666 / http://erichaines.com
7 | */
8 | /*global THREE, console */
9 |
10 | // This set of controls performs orbiting, dollying (zooming), and panning. It maintains
11 | // the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is
12 | // supported.
13 | //
14 | // Orbit - left mouse / touch: one finger move
15 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
16 | // Pan - right mouse, or arrow keys / touch: three finter swipe
17 |
18 | THREE.OrbitControls = function ( object, domElement ) {
19 |
20 | this.object = object;
21 | this.domElement = ( domElement !== undefined ) ? domElement : document;
22 |
23 | // API
24 |
25 | // Set to false to disable this control
26 | this.enabled = true;
27 |
28 | // "target" sets the location of focus, where the control orbits around
29 | // and where it pans with respect to.
30 | this.target = new THREE.Vector3();
31 |
32 | // center is old, deprecated; use "target" instead
33 | this.center = this.target;
34 |
35 | // This option actually enables dollying in and out; left as "zoom" for
36 | // backwards compatibility
37 | this.noZoom = false;
38 | this.zoomSpeed = 1.0;
39 |
40 | // Limits to how far you can dolly in and out ( PerspectiveCamera only )
41 | this.minDistance = 0;
42 | this.maxDistance = Infinity;
43 |
44 | // Limits to how far you can zoom in and out ( OrthographicCamera only )
45 | this.minZoom = 0;
46 | this.maxZoom = Infinity;
47 |
48 | // Set to true to disable this control
49 | this.noRotate = false;
50 | this.rotateSpeed = 1.0;
51 |
52 | // Set to true to disable this control
53 | this.noPan = false;
54 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push
55 |
56 | // Set to true to automatically rotate around the target
57 | this.autoRotate = false;
58 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
59 |
60 | // How far you can orbit vertically, upper and lower limits.
61 | // Range is 0 to Math.PI radians.
62 | this.minPolarAngle = 0; // radians
63 | this.maxPolarAngle = Math.PI; // radians
64 |
65 | // How far you can orbit horizontally, upper and lower limits.
66 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
67 | this.minAzimuthAngle = - Infinity; // radians
68 | this.maxAzimuthAngle = Infinity; // radians
69 |
70 | // Set to true to disable use of the keys
71 | this.noKeys = false;
72 |
73 | // The four arrow keys
74 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
75 |
76 | // Mouse buttons
77 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT };
78 |
79 | ////////////
80 | // internals
81 |
82 | var scope = this;
83 |
84 | var EPS = 0.000001;
85 |
86 | var rotateStart = new THREE.Vector2();
87 | var rotateEnd = new THREE.Vector2();
88 | var rotateDelta = new THREE.Vector2();
89 |
90 | var panStart = new THREE.Vector2();
91 | var panEnd = new THREE.Vector2();
92 | var panDelta = new THREE.Vector2();
93 | var panOffset = new THREE.Vector3();
94 |
95 | var offset = new THREE.Vector3();
96 |
97 | var dollyStart = new THREE.Vector2();
98 | var dollyEnd = new THREE.Vector2();
99 | var dollyDelta = new THREE.Vector2();
100 |
101 | var theta;
102 | var phi;
103 | var phiDelta = 0;
104 | var thetaDelta = 0;
105 | var scale = 1;
106 | var pan = new THREE.Vector3();
107 |
108 | var lastPosition = new THREE.Vector3();
109 | var lastQuaternion = new THREE.Quaternion();
110 |
111 | var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };
112 |
113 | var state = STATE.NONE;
114 |
115 | // for reset
116 |
117 | this.target0 = this.target.clone();
118 | this.position0 = this.object.position.clone();
119 | this.zoom0 = this.object.zoom;
120 |
121 | // so camera.up is the orbit axis
122 |
123 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
124 | var quatInverse = quat.clone().inverse();
125 |
126 | // events
127 |
128 | var changeEvent = { type: 'change' };
129 | var startEvent = { type: 'start' };
130 | var endEvent = { type: 'end' };
131 |
132 | this.rotateLeft = function ( angle ) {
133 |
134 | if ( angle === undefined ) {
135 |
136 | angle = getAutoRotationAngle();
137 |
138 | }
139 |
140 | thetaDelta -= angle;
141 |
142 | };
143 |
144 | this.rotateUp = function ( angle ) {
145 |
146 | if ( angle === undefined ) {
147 |
148 | angle = getAutoRotationAngle();
149 |
150 | }
151 |
152 | phiDelta -= angle;
153 |
154 | };
155 |
156 | // pass in distance in world space to move left
157 | this.panLeft = function ( distance ) {
158 |
159 | var te = this.object.matrix.elements;
160 |
161 | // get X column of matrix
162 | panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] );
163 | panOffset.multiplyScalar( - distance );
164 |
165 | pan.add( panOffset );
166 |
167 | };
168 |
169 | // pass in distance in world space to move up
170 | this.panUp = function ( distance ) {
171 |
172 | var te = this.object.matrix.elements;
173 |
174 | // get Y column of matrix
175 | panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] );
176 | panOffset.multiplyScalar( distance );
177 |
178 | pan.add( panOffset );
179 |
180 | };
181 |
182 | // pass in x,y of change desired in pixel space,
183 | // right and down are positive
184 | this.pan = function ( deltaX, deltaY ) {
185 |
186 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
187 |
188 | if ( scope.object instanceof THREE.PerspectiveCamera ) {
189 |
190 | // perspective
191 | var position = scope.object.position;
192 | var offset = position.clone().sub( scope.target );
193 | var targetDistance = offset.length();
194 |
195 | // half of the fov is center to top of screen
196 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
197 |
198 | // we actually don't use screenWidth, since perspective camera is fixed to screen height
199 | scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight );
200 | scope.panUp( 2 * deltaY * targetDistance / element.clientHeight );
201 |
202 | } else if ( scope.object instanceof THREE.OrthographicCamera ) {
203 |
204 | // orthographic
205 | scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth );
206 | scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight );
207 |
208 | } else {
209 |
210 | // camera neither orthographic or perspective
211 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
212 |
213 | }
214 |
215 | };
216 |
217 | this.dollyIn = function ( dollyScale ) {
218 |
219 | if ( dollyScale === undefined ) {
220 |
221 | dollyScale = getZoomScale();
222 |
223 | }
224 |
225 | if ( scope.object instanceof THREE.PerspectiveCamera ) {
226 |
227 | scale /= dollyScale;
228 |
229 | } else if ( scope.object instanceof THREE.OrthographicCamera ) {
230 |
231 | scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom * dollyScale ) );
232 | scope.object.updateProjectionMatrix();
233 | scope.dispatchEvent( changeEvent );
234 |
235 | } else {
236 |
237 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
238 |
239 | }
240 |
241 | };
242 |
243 | this.dollyOut = function ( dollyScale ) {
244 |
245 | if ( dollyScale === undefined ) {
246 |
247 | dollyScale = getZoomScale();
248 |
249 | }
250 |
251 | if ( scope.object instanceof THREE.PerspectiveCamera ) {
252 |
253 | scale *= dollyScale;
254 |
255 | } else if ( scope.object instanceof THREE.OrthographicCamera ) {
256 |
257 | scope.object.zoom = Math.max( this.minZoom, Math.min( this.maxZoom, this.object.zoom / dollyScale ) );
258 | scope.object.updateProjectionMatrix();
259 | scope.dispatchEvent( changeEvent );
260 |
261 | } else {
262 |
263 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
264 |
265 | }
266 |
267 | };
268 |
269 | this.update = function () {
270 |
271 | var position = this.object.position;
272 |
273 | offset.copy( position ).sub( this.target );
274 |
275 | // rotate offset to "y-axis-is-up" space
276 | offset.applyQuaternion( quat );
277 |
278 | // angle from z-axis around y-axis
279 |
280 | theta = Math.atan2( offset.x, offset.z );
281 |
282 | // angle from y-axis
283 |
284 | phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
285 |
286 | if ( this.autoRotate && state === STATE.NONE ) {
287 |
288 | this.rotateLeft( getAutoRotationAngle() );
289 |
290 | }
291 |
292 | theta += thetaDelta;
293 | phi += phiDelta;
294 |
295 | // restrict theta to be between desired limits
296 | theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) );
297 |
298 | // restrict phi to be between desired limits
299 | phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
300 |
301 | // restrict phi to be betwee EPS and PI-EPS
302 | phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
303 |
304 | var radius = offset.length() * scale;
305 |
306 | // restrict radius to be between desired limits
307 | radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
308 |
309 | // move target to panned location
310 | this.target.add( pan );
311 |
312 | offset.x = radius * Math.sin( phi ) * Math.sin( theta );
313 | offset.y = radius * Math.cos( phi );
314 | offset.z = radius * Math.sin( phi ) * Math.cos( theta );
315 |
316 | // rotate offset back to "camera-up-vector-is-up" space
317 | offset.applyQuaternion( quatInverse );
318 |
319 | position.copy( this.target ).add( offset );
320 |
321 | this.object.lookAt( this.target );
322 |
323 | thetaDelta = 0;
324 | phiDelta = 0;
325 | scale = 1;
326 | pan.set( 0, 0, 0 );
327 |
328 | // update condition is:
329 | // min(camera displacement, camera rotation in radians)^2 > EPS
330 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8
331 |
332 | if ( lastPosition.distanceToSquared( this.object.position ) > EPS
333 | || 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) {
334 |
335 | this.dispatchEvent( changeEvent );
336 |
337 | lastPosition.copy( this.object.position );
338 | lastQuaternion.copy (this.object.quaternion );
339 |
340 | }
341 |
342 | };
343 |
344 |
345 | this.reset = function () {
346 |
347 | state = STATE.NONE;
348 |
349 | this.target.copy( this.target0 );
350 | this.object.position.copy( this.position0 );
351 | this.object.zoom = this.zoom0;
352 |
353 | this.object.updateProjectionMatrix();
354 | this.dispatchEvent( changeEvent );
355 |
356 | this.update();
357 |
358 | };
359 |
360 | this.getPolarAngle = function () {
361 |
362 | return phi;
363 |
364 | };
365 |
366 | this.getAzimuthalAngle = function () {
367 |
368 | return theta
369 |
370 | };
371 |
372 | function getAutoRotationAngle() {
373 |
374 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
375 |
376 | }
377 |
378 | function getZoomScale() {
379 |
380 | return Math.pow( 0.95, scope.zoomSpeed );
381 |
382 | }
383 |
384 | function onMouseDown( event ) {
385 |
386 | if ( scope.enabled === false ) return;
387 | event.preventDefault();
388 |
389 | if ( event.button === scope.mouseButtons.ORBIT ) {
390 | if ( scope.noRotate === true ) return;
391 |
392 | state = STATE.ROTATE;
393 |
394 | rotateStart.set( event.clientX, event.clientY );
395 |
396 | } else if ( event.button === scope.mouseButtons.ZOOM ) {
397 | if ( scope.noZoom === true ) return;
398 |
399 | state = STATE.DOLLY;
400 |
401 | dollyStart.set( event.clientX, event.clientY );
402 |
403 | } else if ( event.button === scope.mouseButtons.PAN ) {
404 | if ( scope.noPan === true ) return;
405 |
406 | state = STATE.PAN;
407 |
408 | panStart.set( event.clientX, event.clientY );
409 |
410 | }
411 |
412 | if ( state !== STATE.NONE ) {
413 | document.addEventListener( 'mousemove', onMouseMove, false );
414 | document.addEventListener( 'mouseup', onMouseUp, false );
415 | scope.dispatchEvent( startEvent );
416 | }
417 |
418 | }
419 |
420 | function onMouseMove( event ) {
421 |
422 | if ( scope.enabled === false ) return;
423 |
424 | event.preventDefault();
425 |
426 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
427 |
428 | if ( state === STATE.ROTATE ) {
429 |
430 | if ( scope.noRotate === true ) return;
431 |
432 | rotateEnd.set( event.clientX, event.clientY );
433 | rotateDelta.subVectors( rotateEnd, rotateStart );
434 |
435 | // rotating across whole screen goes 360 degrees around
436 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
437 |
438 | // rotating up and down along whole screen attempts to go 360, but limited to 180
439 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
440 |
441 | rotateStart.copy( rotateEnd );
442 |
443 | } else if ( state === STATE.DOLLY ) {
444 |
445 | if ( scope.noZoom === true ) return;
446 |
447 | dollyEnd.set( event.clientX, event.clientY );
448 | dollyDelta.subVectors( dollyEnd, dollyStart );
449 |
450 | if ( dollyDelta.y > 0 ) {
451 |
452 | scope.dollyIn();
453 |
454 | } else if ( dollyDelta.y < 0 ) {
455 |
456 | scope.dollyOut();
457 |
458 | }
459 |
460 | dollyStart.copy( dollyEnd );
461 |
462 | } else if ( state === STATE.PAN ) {
463 |
464 | if ( scope.noPan === true ) return;
465 |
466 | panEnd.set( event.clientX, event.clientY );
467 | panDelta.subVectors( panEnd, panStart );
468 |
469 | scope.pan( panDelta.x, panDelta.y );
470 |
471 | panStart.copy( panEnd );
472 |
473 | }
474 |
475 | if ( state !== STATE.NONE ) scope.update();
476 |
477 | }
478 |
479 | function onMouseUp( /* event */ ) {
480 |
481 | if ( scope.enabled === false ) return;
482 |
483 | document.removeEventListener( 'mousemove', onMouseMove, false );
484 | document.removeEventListener( 'mouseup', onMouseUp, false );
485 | scope.dispatchEvent( endEvent );
486 | state = STATE.NONE;
487 |
488 | }
489 |
490 | function onMouseWheel( event ) {
491 |
492 | if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return;
493 |
494 | event.preventDefault();
495 | event.stopPropagation();
496 |
497 | var delta = 0;
498 |
499 | if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9
500 |
501 | delta = event.wheelDelta;
502 |
503 | } else if ( event.detail !== undefined ) { // Firefox
504 |
505 | delta = - event.detail;
506 |
507 | }
508 |
509 | if ( delta > 0 ) {
510 |
511 | scope.dollyOut();
512 |
513 | } else if ( delta < 0 ) {
514 |
515 | scope.dollyIn();
516 |
517 | }
518 |
519 | scope.update();
520 | scope.dispatchEvent( startEvent );
521 | scope.dispatchEvent( endEvent );
522 |
523 | }
524 |
525 | function onKeyDown( event ) {
526 |
527 | if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return;
528 |
529 | switch ( event.keyCode ) {
530 |
531 | case scope.keys.UP:
532 | scope.pan( 0, scope.keyPanSpeed );
533 | scope.update();
534 | break;
535 |
536 | case scope.keys.BOTTOM:
537 | scope.pan( 0, - scope.keyPanSpeed );
538 | scope.update();
539 | break;
540 |
541 | case scope.keys.LEFT:
542 | scope.pan( scope.keyPanSpeed, 0 );
543 | scope.update();
544 | break;
545 |
546 | case scope.keys.RIGHT:
547 | scope.pan( - scope.keyPanSpeed, 0 );
548 | scope.update();
549 | break;
550 |
551 | }
552 |
553 | }
554 |
555 | function touchstart( event ) {
556 |
557 | if ( scope.enabled === false ) return;
558 |
559 | switch ( event.touches.length ) {
560 |
561 | case 1: // one-fingered touch: rotate
562 |
563 | if ( scope.noRotate === true ) return;
564 |
565 | state = STATE.TOUCH_ROTATE;
566 |
567 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
568 | break;
569 |
570 | case 2: // two-fingered touch: dolly
571 |
572 | if ( scope.noZoom === true ) return;
573 |
574 | state = STATE.TOUCH_DOLLY;
575 |
576 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
577 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
578 | var distance = Math.sqrt( dx * dx + dy * dy );
579 | dollyStart.set( 0, distance );
580 | break;
581 |
582 | case 3: // three-fingered touch: pan
583 |
584 | if ( scope.noPan === true ) return;
585 |
586 | state = STATE.TOUCH_PAN;
587 |
588 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
589 | break;
590 |
591 | default:
592 |
593 | state = STATE.NONE;
594 |
595 | }
596 |
597 | if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent );
598 |
599 | }
600 |
601 | function touchmove( event ) {
602 |
603 | if ( scope.enabled === false ) return;
604 |
605 | event.preventDefault();
606 | event.stopPropagation();
607 |
608 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement;
609 |
610 | switch ( event.touches.length ) {
611 |
612 | case 1: // one-fingered touch: rotate
613 |
614 | if ( scope.noRotate === true ) return;
615 | if ( state !== STATE.TOUCH_ROTATE ) return;
616 |
617 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
618 | rotateDelta.subVectors( rotateEnd, rotateStart );
619 |
620 | // rotating across whole screen goes 360 degrees around
621 | scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed );
622 | // rotating up and down along whole screen attempts to go 360, but limited to 180
623 | scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed );
624 |
625 | rotateStart.copy( rotateEnd );
626 |
627 | scope.update();
628 | break;
629 |
630 | case 2: // two-fingered touch: dolly
631 |
632 | if ( scope.noZoom === true ) return;
633 | if ( state !== STATE.TOUCH_DOLLY ) return;
634 |
635 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
636 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
637 | var distance = Math.sqrt( dx * dx + dy * dy );
638 |
639 | dollyEnd.set( 0, distance );
640 | dollyDelta.subVectors( dollyEnd, dollyStart );
641 |
642 | if ( dollyDelta.y > 0 ) {
643 |
644 | scope.dollyOut();
645 |
646 | } else if ( dollyDelta.y < 0 ) {
647 |
648 | scope.dollyIn();
649 |
650 | }
651 |
652 | dollyStart.copy( dollyEnd );
653 |
654 | scope.update();
655 | break;
656 |
657 | case 3: // three-fingered touch: pan
658 |
659 | if ( scope.noPan === true ) return;
660 | if ( state !== STATE.TOUCH_PAN ) return;
661 |
662 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
663 | panDelta.subVectors( panEnd, panStart );
664 |
665 | scope.pan( panDelta.x, panDelta.y );
666 |
667 | panStart.copy( panEnd );
668 |
669 | scope.update();
670 | break;
671 |
672 | default:
673 |
674 | state = STATE.NONE;
675 |
676 | }
677 |
678 | }
679 |
680 | function touchend( /* event */ ) {
681 |
682 | if ( scope.enabled === false ) return;
683 |
684 | scope.dispatchEvent( endEvent );
685 | state = STATE.NONE;
686 |
687 | }
688 |
689 | this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
690 | this.domElement.addEventListener( 'mousedown', onMouseDown, false );
691 | this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
692 | this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
693 |
694 | this.domElement.addEventListener( 'touchstart', touchstart, false );
695 | this.domElement.addEventListener( 'touchend', touchend, false );
696 | this.domElement.addEventListener( 'touchmove', touchmove, false );
697 |
698 | window.addEventListener( 'keydown', onKeyDown, false );
699 |
700 | // force an update at start
701 | this.update();
702 |
703 | };
704 |
705 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
706 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls;
707 |
--------------------------------------------------------------------------------
/resources/public/libs/d3.layout.force3d.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | // D3.layout.force3d.js
3 | // (C) 2012 ziggy.jonsson.nyc@gmail.com
4 | // BSD license (http://opensource.org/licenses/BSD-3-Clause)
5 |
6 | d3.layout.force3d = function() {
7 | var forceXY = d3.layout.force()
8 | ,forceZ = d3.layout.force()
9 | ,zNodes = {}
10 | ,zLinks = {}
11 | ,nodeID = 1
12 | ,linkID = 1
13 | ,tickFunction = Object
14 |
15 | var force3d = {}
16 |
17 | Object.keys(forceXY).forEach(function(d) {
18 | force3d[d] = function() {
19 | var result = forceXY[d].apply(this,arguments)
20 | if (d !="nodes" && d!="links") forceZ[d].apply(this,arguments)
21 | return (result == forceXY) ? force3d : result
22 | }
23 | })
24 |
25 |
26 | force3d.on = function(name,fn) {
27 | if(name == "tick"){
28 | tickFunction = fn;
29 | }
30 | else{
31 | forceXY.on(name, fn);
32 | }
33 | return force3d
34 | }
35 |
36 |
37 | forceXY.on("tick",function() {
38 |
39 | // Refresh zNodes add new, delete removed
40 | var _zNodes = {}
41 | forceXY.nodes().forEach(function(d,i) {
42 | if (!d.id) d.id = nodeID++
43 | _zNodes[d.id] = zNodes[d.id] || {x:d.z,px:d.z,py:d.z,y:d.z,id:d.id}
44 | d.z = _zNodes[d.id].x
45 | })
46 | zNodes = _zNodes
47 |
48 | // Refresh zLinks add new, delete removed
49 | var _zLinks = {}
50 | forceXY.links().forEach(function(d) {
51 | var nytt = false
52 | if (!d.linkID) { d.linkID = linkID++;nytt=true}
53 | _zLinks[d.linkID] = zLinks[d.linkID] || {target:zNodes[d.target.id],source:zNodes[d.source.id]}
54 |
55 | })
56 | zLinks = _zLinks
57 |
58 | // Update the nodes/links in forceZ
59 | forceZ.nodes(d3.values(zNodes))
60 | forceZ.links(d3.values(zLinks))
61 | forceZ.start() // Need to kick forceZ so we don't lose the update mechanism
62 |
63 | // And run the user defined function, if defined
64 | if(tickFunction) tickFunction();
65 | })
66 |
67 | // Expose the sub-forces for debugging purposes
68 | force3d.xy = forceXY
69 | force3d.z = forceZ
70 |
71 | return force3d
72 | }
73 | })()
--------------------------------------------------------------------------------
/resources/public/libs/stats.min.js:
--------------------------------------------------------------------------------
1 | // stats.js - http://github.com/mrdoob/stats.js
2 | var Stats=function(){function f(a,e,b){a=document.createElement(a);a.id=e;a.style.cssText=b;return a}function l(a,e,b){var c=f("div",a,"padding:0 0 3px 3px;text-align:left;background:"+b),d=f("div",a+"Text","font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px;color:"+e);d.innerHTML=a.toUpperCase();c.appendChild(d);a=f("div",a+"Graph","width:74px;height:30px;background:"+e);c.appendChild(a);for(e=0;74>e;e++)a.appendChild(f("span","","width:1px;height:30px;float:left;opacity:0.9;background:"+
3 | b));return c}function m(a){for(var b=c.children,d=0;dr+1E3&&(d=Math.round(1E3*
5 | t/(a-r)),u=Math.min(u,d),v=Math.max(v,d),A.textContent=d+" FPS ("+u+"-"+v+")",p(B,d/100),r=a,t=0,void 0!==h)){var b=performance.memory.usedJSHeapSize,c=performance.memory.jsHeapSizeLimit;h=Math.round(9.54E-7*b);y=Math.min(y,h);z=Math.max(z,h);E.textContent=h+" MB ("+y+"-"+z+")";p(F,b/c)}return a},update:function(){k=this.end()}}};"object"===typeof module&&(module.exports=Stats);
6 |
--------------------------------------------------------------------------------
/src/gravity/events.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.events
2 | (:require-macros [cljs.core.async.macros :refer [go]])
3 | (:require [cljs.core.async :refer [! chan]]
4 | [gravity.tools :refer [log]]))
5 |
6 |
7 |
8 | (defn create-chan
9 | "Return a simple chan
10 | - avoiding a :require into core
11 | - centralizing the creation"
12 | []
13 | (chan 512))
14 |
15 |
16 | (defn create-store
17 | "Create an atom and two closures ':on' and ':get-callbacks'.
18 | - Use :on to store a callback
19 | - Use :get-callbacks to retrive a deref of the callbacks atom map.
20 | Warning : the keys are transformed to keywords."
21 | []
22 | (let [callbacks-map (atom {})]
23 |
24 | ;;return 'on'
25 | {:on (fn [cb-key callback]
26 | (swap! callbacks-map assoc (keyword cb-key) callback))
27 | :off (fn
28 | ([]
29 | (reset! callbacks-map {}))
30 | ([cb-key]
31 | (swap! callbacks-map assoc (keyword cb-key) nil)) )
32 |
33 | :get-callbacks (fn [] @callbacks-map)}))
34 |
35 |
36 |
37 |
38 | ;; Events triggers
39 |
40 | (defn- get-callbacks
41 | "Extract and execute the :get-callbacks closure from the store"
42 | [store]
43 | (let [deref-callbacks (:get-callbacks store)]
44 | (deref-callbacks)))
45 |
46 |
47 |
48 | (defn- call
49 | [callback & args]
50 | (when-not (nil? callback)
51 | (apply callback args)))
52 |
53 |
54 | (defn- trigger
55 | "Trigger an event taking no arguments"
56 | ([callback-key store]
57 | (trigger callback-key store nil nil))
58 |
59 | ([callback-key store object-to-pass]
60 | (trigger callback-key store object-to-pass nil))
61 |
62 | ([callback-key store object-to-pass original-event]
63 | (let [store (get-callbacks store)
64 | callback (callback-key store)]
65 | (call callback object-to-pass original-event))))
66 |
67 |
68 |
69 |
70 |
71 |
72 | ;; dispatcher
73 |
74 | (defn listen-outgoing-events
75 | "Listen to an output chan and trigger the appropriate callbacks if found in the events-store."
76 | [chan-out store]
77 | (let [state (atom {:target nil})]
78 | (go
79 | (while true
80 | (let [event (js {:type flag})))
12 | ([worker flag data]
13 | (.postMessage worker (clj->js { :type flag
14 | :data data}))))
15 |
16 |
17 | (defn make-worker!
18 | [path]
19 | (log (str "A worker will be created to the given path '" path "'. If the canvas stay empty, be sure to check the path."))
20 | (new js/Worker path))
21 |
22 | (defn create
23 | "Create a webworker with the given script path"
24 | [path params]
25 | (when-not path
26 | (warn (str "Invalid worker path ! Unable to create a graph without a correct worker file specified. (null)")))
27 | (let [worker (make-worker! path)]
28 | (if worker
29 | (send worker :init params)
30 | (err (str "Invalid worker for path '" path "'. Graph creation will fail.")))
31 | worker))
32 |
33 | (defn listen
34 | "Listen to a given worker by setting the given function as a callback of onMessage"
35 | [worker callback]
36 | (.addEventListener worker "message" callback))
37 |
38 |
39 | (defn serialize
40 | "Serialize a value (number, function, etc…) to be evaluated by another thread"
41 | [value]
42 | (str "(" (.toString value) ")"))
43 |
--------------------------------------------------------------------------------
/src/gravity/force/worker.cljs:
--------------------------------------------------------------------------------
1 | ;; (when-not (undefined? js/self.importScripts)
2 | ;; (.importScripts js/self "../libs/d3.js" "../libs/d3.layout.force3d.js"))
3 |
4 | ;;---------------------------------
5 |
6 | (ns gravity.force.worker
7 | (:refer-clojure :exclude [str force])
8 | (:import [goog.object]))
9 |
10 | (defn answer
11 | "Post a message back"
12 | ([message]
13 | (.postMessage js/self (clj->js message)))
14 | ([message data]
15 | (.postMessage js/self (clj->js message) (clj->js data))))
16 |
17 |
18 |
19 |
20 | (defn- get-args
21 | "Return the first arg or all the list as a js-obj"
22 | [coll]
23 | (if (= (count coll) 1)
24 | (clj->js (first coll))
25 | (clj->js coll)))
26 |
27 | (defn log
28 | "Log in the console"
29 | [args]
30 | (.log js/console "[force.worker/log]: " (get-args args)))
31 |
32 | (defn warn
33 | "Warn in the console"
34 | [args]
35 | (.warn js/console "[force.worker/warn]: " (get-args args)))
36 |
37 | (defn str
38 | [& args]
39 | (let [arr (clj->js args)]
40 | (.join arr "")))
41 |
42 | (defn eval
43 | [value]
44 | (js/eval value))
45 |
46 |
47 | (def force (atom nil))
48 | (def parameters (atom nil))
49 |
50 | ;; --------------------------------
51 |
52 |
53 |
54 |
55 | (defn tick
56 | "Tick function for the force layout"
57 | [_]
58 | (let [nodes (.nodes @force)
59 | size (.-length nodes)]
60 | ;;(log [(first nodes)])
61 | (when (> size 0)
62 | (let [arr (new js/Float32Array (* size 3))
63 | buffer (.-buffer arr)]
64 | (loop [i 0]
65 | (let [j (* i 3)
66 | node (aget nodes i)]
67 | (aset arr j (.-x node))
68 | (aset arr (+ j 1) (.-y node))
69 | (if (js/isNaN (.-z node))
70 | (aset arr (+ j 2) 0)
71 | (aset arr (+ j 2) (.-z node))))
72 | (when (< i (dec size))
73 | (recur (inc i))))
74 |
75 | (answer {:type "nodes-positions" :data arr} [buffer])))))
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | (defn init
87 | [params]
88 | (let [params (js->clj params :keywordize-keys true)]
89 | (reset! parameters params)
90 | (answer {:type :ready})
91 | nil))
92 |
93 | (defn make-force
94 | []
95 | (let [params @parameters
96 | force-instance (-> (.force3d js/d3.layout)
97 | (.size (clj->js (:size params)))
98 | (.linkStrength (:linkStrength params))
99 | (.friction (:friction params))
100 | (.linkDistance (:linkDistance params))
101 | (.charge (:charge params))
102 | (.gravity (:gravity params))
103 | (.theta (:theta params))
104 | (.alpha (:alpha params))
105 | )]
106 | (.on force-instance "tick" tick)
107 | force-instance))
108 |
109 |
110 | (defn- update-nodes-array
111 | "Add or remove the correct amount of nodes and keep their positions"
112 | [current-array nb-nodes]
113 | (let [size (count current-array)]
114 | (if (= size nb-nodes)
115 | current-array
116 | ;else
117 | (if (< size nb-nodes)
118 | (let [diff (- nb-nodes size)]
119 | (doseq [i (range 0 diff)]
120 | (.push current-array #js {}))
121 | current-array)
122 | ;else (> size nb-nodes)
123 | (let [diff (- size nb-nodes)]
124 | (.splice current-array nb-nodes diff)
125 | current-array)))))
126 |
127 |
128 |
129 | (defn start
130 | "start the force"
131 | []
132 | (when-not (nil? @force)
133 | (.start @force)))
134 |
135 | (defn stop
136 | "Stop the force"
137 | []
138 | (when-not (nil? @force)
139 | (.stop @force)))
140 |
141 | (defn resume
142 | "Resume the force"
143 | []
144 | (when-not (nil? @force)
145 | (.resume @force)))
146 |
147 | (defn set-nodes
148 | "Set the nodes list"
149 | [nb-nodes]
150 | (stop)
151 | (let [new-force (make-force)
152 | nodes (if-not (nil? @force)
153 | (.nodes @force)
154 | (array))
155 | nodes (update-nodes-array nodes nb-nodes)]
156 | (.nodes new-force nodes)
157 | (reset! force new-force)
158 | (start)))
159 |
160 |
161 | (defn set-links
162 | "Set the links list"
163 | [links]
164 | (stop)
165 | (when-not (nil? @force)
166 | (.links @force links)
167 | (start)))
168 |
169 |
170 | (defn precompute
171 | "Force the layout to precompute"
172 | [steps]
173 | (if (or (< steps 0) (nil? steps))
174 | (do
175 | (.log js/console "Precomputing layout with default value. Argument given was <0. Expected unsigned integer, Given:" steps )
176 | (precompute 50))
177 | (do
178 | (let [start (.now js/Date)]
179 | (.on @force "tick" nil)
180 | (dotimes [i steps]
181 | (.tick @force))
182 | (.on @force "tick" tick)
183 | (log (str "Pre-computed in " (/ (- (.now js/Date) start) 1000) "ms.")))
184 | )))
185 |
186 |
187 |
188 |
189 |
190 |
191 | (defn set-position
192 | "Set a node's position"
193 | [data]
194 | (let [index (-> data .-index)
195 | position (-> data .-position)
196 | node (aget (.nodes @force) index)
197 | alpha (.alpha @force)]
198 |
199 | (stop)
200 |
201 | ;;(when-not (> alpha 0)
202 | ;;(.alpha @force 0.01))
203 |
204 | (set! (.-x node) (.-x position))
205 | (set! (.-y node) (.-y position))
206 | (set! (.-z node) (.-z position))
207 |
208 |
209 | (set! (.-px node) (.-x position))
210 | (set! (.-py node) (.-y position))
211 | (set! (.-pz node) (.-z position))
212 |
213 | ;;(set! (.-fixed node) false)
214 |
215 | (tick nil)
216 | ))
217 |
218 |
219 | (defn pin
220 | "pin a node by index"
221 | [data]
222 | (let [index (-> data .-index)
223 | node (aget (.nodes @force) index)]
224 | (set! (.-fixed node) true)))
225 |
226 |
227 | (defn unpin
228 | "unpin a node by index"
229 | [data]
230 | (let [index (-> data .-index)
231 | node (aget (.nodes @force) index)]
232 | (set! (.-fixed node) false)))
233 |
234 |
235 |
236 |
237 | (defn dispatcher
238 | "Dispatch a message to the corresponding action (route)."
239 | [event]
240 |
241 | (let [message (.-data event)
242 | type (.-type message)
243 | data (.-data message)]
244 | (case type
245 | "init" (init data)
246 | "start" (start)
247 | "stop" (stop)
248 | "resume" (resume)
249 | "tick" (tick nil)
250 | "set-nodes" (set-nodes data)
251 | "set-links" (set-links data)
252 | "precompute" (precompute data)
253 |
254 | "set-position" (set-position data)
255 | "pin" (pin data)
256 | "unpin" (unpin data)
257 |
258 | ;set params
259 | "size" (swap! parameters assoc :size (js->clj data))
260 | "linkStrength" (swap! parameters assoc :linkStrength (eval data))
261 | "friction" (swap! parameters assoc :friction data)
262 | "linkDistance" (swap! parameters assoc :linkDistance (eval data))
263 | "charge" (swap! parameters assoc :charge (eval data))
264 | "gravity" (swap! parameters assoc :gravity data)
265 | "theta" (swap! parameters assoc :theta data)
266 | "alpha" (swap! parameters assoc :alpha data)
267 |
268 | (warn (str "Unable to dispatch '" type "'")))))
269 |
270 |
271 | ;; :size [1 1]
272 | ;; :linkStrength 1
273 | ;; :friction 0.9
274 | ;; :linkDistance 20
275 | ;; :charge -30
276 | ;; :gravity 0.1
277 | ;; :theta 0.8
278 | ;; :alpha 0.1
279 |
280 |
281 |
282 | (defn ^:export create
283 | "Main entry point"
284 | []
285 | (.addEventListener js/self "message" dispatcher))
286 |
--------------------------------------------------------------------------------
/src/gravity/graph.cljs:
--------------------------------------------------------------------------------
1 | (ns ^:figwheel-always gravity.graph
2 | (:require [gravity.view.graph :as graph]
3 | [gravity.view.graph-tools :as tools]
4 | [gravity.events :as events]
5 | [gravity.force.proxy :as worker]
6 | [gravity.force.worker :as webworker]
7 | [gravity.tools :refer [log]]))
8 |
9 | (enable-console-print!)
10 |
11 | (defonce app-state (atom {}))
12 |
13 |
14 | (def default-parameters {:color (.category10 js/d3.scale)
15 | :worker-path "./gravity-worker.js"
16 | :stats false
17 | :force {:size [1 1 1]
18 | :linkStrength 1
19 | :friction 0.9
20 | :linkDistance 20
21 | :charge -30
22 | :gravity 0.1
23 | :theta 0.8
24 | :alpha 0.1}
25 | :webgl {:antialias true
26 | :background false
27 | :lights true
28 | :shadows true}})
29 |
30 |
31 |
32 | (defn bind-dev-events
33 | [graph]
34 |
35 | (let [{on :on
36 | canvas :canvas} graph]
37 | (on "node-over" (fn [node]
38 | (log :over)
39 | (set! (-> canvas .-style .-cursor) "pointer")))
40 | (on "node-blur" (fn []
41 | (log :blur)
42 | (set! (-> canvas .-style .-cursor) "inherit")))
43 | (on "node-select" (fn [node]
44 | (log :void)
45 | (log [:select (.-name node) node])))
46 | (on "void-click" (fn []
47 | (log [:void])))
48 | (on "node-click" (fn [node]
49 | (log :node-click)
50 | (let [select (:selectNode graph)]
51 | (select node))))
52 | (on "node-dbl-click" (fn [node]
53 | (log :dbl-click)
54 | (let [unpin (:unpinNode graph)
55 | resume (:resume graph)]
56 | (unpin node)
57 | (resume))))
58 | (on "drag-start" (fn [node]
59 | (log :drag-start)))
60 | (on "drag-end" (fn [node]
61 | (log :drag-end)
62 | (log node)
63 | (let [pin (:pinNode graph)
64 | resume (:resume graph)]
65 | (pin node)
66 | (resume))))
67 | (on "ready" (fn []
68 | (let [set-nodes (:nodes graph)
69 | set-links (:links graph)
70 | update-force (:updateForce graph)
71 |
72 | nodes (clj->js [{:name "foo" :group 0} {:name "bar" :group 1}])
73 | links (clj->js [{:source 0 :target 1}])]
74 | (set-nodes nodes)
75 | (set-links links)
76 | (update-force)
77 | )))))
78 |
79 |
80 | (defn unbind-old-events
81 | [last-instance]
82 | (let [off (-> last-instance .-off)]
83 | (when-not (nil? off)
84 | (off))))
85 |
86 |
87 |
88 | (defn init-parameters
89 | [user-map]
90 | (let [user-map (js->clj user-map :keywordize-keys true)
91 | webgl-params (:webgl user-map)
92 | force-params (:force user-map)
93 | color (if (:color user-map)
94 | (:color user-map)
95 | (:color default-parameters))
96 | force-merged (merge (:force default-parameters) force-params)
97 | webgl-merged (merge (:webgl default-parameters) webgl-params)]
98 | {:color color
99 | :force force-merged
100 | :webgl webgl-merged}))
101 |
102 |
103 | (defn- main
104 |
105 | ([user-map]
106 | (let [graph (main user-map false)]
107 | (clj->js graph)))
108 |
109 | ([user-map dev-mode]
110 |
111 | (let [chan-out (events/create-chan)
112 | store (events/create-store)
113 | graph (graph/create user-map chan-out dev-mode) ;; <--
114 | graph (-> graph
115 | (merge store)
116 | (dissoc :get-callbacks))]
117 | (events/listen-outgoing-events chan-out store)
118 | (bind-dev-events graph)
119 | graph)))
120 |
121 |
122 |
123 |
124 | (defn on-js-reload
125 | ([]
126 | (let [state @app-state
127 | last-instance (:last-instance state)]
128 |
129 | (when-not (or (nil? last-instance) (empty? last-instance))
130 | (unbind-old-events last-instance)
131 | (apply (:stop last-instance) []))
132 |
133 |
134 | (let [graph (main state true)]
135 | (swap! app-state assoc-in [:last-instance] graph)
136 | (swap! app-state assoc :first-run false)
137 |
138 | graph))))
139 |
140 |
141 |
142 |
143 | (defn ^:export init-dev-mode
144 | "Set some params to use live-reload in dev mode"
145 | [user-map]
146 | (let [user-map (js->clj user-map :keywordize-keys true)
147 | dev-app-state {:stats (tools/make-stats)
148 | :last-instance {}
149 | :first-run true}
150 | params (init-parameters user-map)
151 | state (merge dev-app-state user-map params)]
152 | (reset! app-state state)
153 | (swap! app-state assoc :force-worker (worker/create "force-worker/worker.js" (:force state)))
154 |
155 | (clj->js
156 | (on-js-reload))))
157 |
158 |
159 |
160 | (defn ^:export create
161 | [user-map]
162 | (let [user-map (js->clj user-map :keywordize-keys true)
163 | params (init-parameters user-map)
164 | state (if (:stats user-map)
165 | (merge user-map params {:stats (tools/make-stats)})
166 | (merge user-map params))
167 |
168 | graph (main state false)]
169 | (clj->js graph)))
170 |
171 | (def ^:export create-worker gravity.force.worker/create)
172 |
--------------------------------------------------------------------------------
/src/gravity/tools.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.tools)
2 |
3 | (defn- get-args
4 | "Return the first arg or all the list as a js-obj"
5 | [coll]
6 | (if (= (count coll) 1)
7 | (clj->js (first coll))
8 | (clj->js coll)))
9 |
10 |
11 | (defn log
12 | "Log in the console"
13 | [& args]
14 | (.log js/console (get-args args))
15 | )
16 |
17 | (defn warn
18 | "Warn in the console"
19 | [& args]
20 | (.warn js/console (get-args args)))
21 |
22 | (defn err
23 | "Error in the console"
24 | [& args]
25 | (.error js/console (get-args args)))
26 |
--------------------------------------------------------------------------------
/src/gravity/view/events_generator.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.view.events-generator
2 | "Events listeners on the canvas, mouse, etc…"
3 | (:require
4 | [gravity.tools :refer [log]]
5 | [gravity.view.graph-tools :as tools]
6 | [gravity.force.proxy :as force :refer [send]]
7 | [cljs.core.async :refer [chan >! (.-clientX event)
40 | (- (.-left bounding-rect))
41 | (/ scene-width);;(.-offsetWidth canvas))
42 | (* 2)
43 | (- 1))
44 | y (-> (.-clientY event)
45 | (- (.-top bounding-rect))
46 | (-)
47 | (/ scene-height);;(.-offsetHeight canvas))
48 | (* 2)
49 | (+ 1))
50 | cam-position (.-position camera)]
51 | (.set mouse-pos x y 1)
52 | (.unproject mouse-pos camera)
53 | (.set raycaster cam-position (.normalize (.sub mouse-pos cam-position)))
54 | ;;return
55 | (first (.intersectObjects raycaster objects)))))
56 |
57 |
58 |
59 |
60 |
61 |
62 | (defn- move
63 | "Callback for the mouseMove event on the canvas node"
64 | [event canvas camera raycaster state chan events-state controls intersect-plane]
65 | (let [colliders (:meshes @state)
66 | target (get-target event canvas camera raycaster colliders)]
67 | (if-not (nil? target)
68 | (let [node (.-node (.-object target))]
69 | ;; disable controls
70 | (set! (-> controls .-enabled) false)
71 | ;; move plane
72 | (.copy (-> intersect-plane .-position) (-> node .-position))
73 | (.lookAt intersect-plane (-> camera .-position))
74 | ;; send event to the user
75 | (when-not (= :node-over (:last @events-state))
76 | (swap! events-state assoc :last :node-over)
77 | (go
78 | (>! chan {:type :node-over
79 | :target node
80 | :original-event event}))))
81 | ;else
82 | (when (or
83 | (= :node-over (:last @events-state))
84 | (= :drag (:last @events-state))
85 | (= :up (:last @events-state)))
86 | (set! (-> controls .-enabled) true)
87 | (swap! events-state assoc :last :blur)
88 | (go (>! chan {:type :node-blur
89 | :original-event event}))))))
90 |
91 |
92 | (defn- click
93 | "click event"
94 | [event canvas camera raycaster state chan]
95 | (log "click")
96 | (let [colliders (:meshes @state)
97 | target (get-target event canvas camera raycaster colliders)]
98 | (when-not (nil? target)
99 | (let [node (-> target .-object .-node)]
100 | (go (>! chan {:type :node-click
101 | :target node
102 | :original-event event}))))
103 | false))
104 |
105 |
106 | (defn double-click
107 | "Callback for the click event"
108 | [event canvas camera raycaster state chan]
109 |
110 | (let [colliders (:meshes @state)
111 | target (get-target event canvas camera raycaster colliders)]
112 | (when-not (nil? target)
113 | (let [node (.-node (.-object target))]
114 | (go (>! chan {:type :node-dbl-click
115 | :target node
116 | :original-event event})))))
117 | false)
118 |
119 |
120 |
121 |
122 |
123 | (defn- drag
124 | [event canvas camera raycaster events-state intersect-plane force-worker chan-out]
125 | (let [node (:target @events-state)]
126 | (when-not (nil? node)
127 | (let [node (-> node .-object)
128 | index (-> node .-node .-index)
129 | intersect (get-target event canvas camera raycaster (array intersect-plane))]
130 | (when-not (nil? intersect)
131 | ;;(.copy (-> node .-position) (-> intersect .-point))
132 | (force/send force-worker :set-position {:index index
133 | :position (-> intersect .-point)})
134 | (when (= :down (:last @events-state))
135 | (force/send force-worker "stop")
136 | (go (>! chan-out {:type :drag-start
137 | :target node
138 | :original-event event})))
139 | (swap! events-state assoc :last :drag)
140 | )))))
141 |
142 |
143 |
144 |
145 | (defn- down
146 | [event canvas camera raycaster state events-state force-worker]
147 | (let [colliders (:meshes @state)
148 | target (get-target event canvas camera raycaster colliders)]
149 | (when-not (nil? target)
150 | (force/send force-worker "stop")
151 | (swap! events-state assoc :last :down)
152 | (swap! events-state assoc :target target))))
153 |
154 |
155 | (defn- up
156 | [event events-state force-worker chan-out]
157 | (when (= :drag (:last @events-state))
158 | (let [target (:target @events-state)]
159 | (log "drag-end-before")
160 | (when-not (nil? target)
161 | (log "drag-end-with-target")
162 | (go (>! chan-out {:type :drag-end
163 | :target (-> target .-object .-node)
164 | :original-event event})))))
165 | (swap! events-state assoc :last :up)
166 | (swap! events-state dissoc :target))
167 |
168 |
169 |
170 |
171 | (defn notify-user-ready
172 | [chan]
173 | (go (>! chan {:type :ready})))
174 |
175 |
176 |
177 |
178 |
179 |
180 | ;; Events factory
181 |
182 |
183 |
184 | (defn- listen-to-mouse-events
185 | "Take chans with events from the dom and alt! them to generate meaningful events."
186 | [mouse-down mouse-up mouse-move]
187 | (let [timeout-time 350
188 |
189 | out-chan (chan 10)
190 | events-state (atom {})]
191 | (go-loop []
192 | (loop []
193 | (alt! [mouse-down] ([transducted] (do
194 | (swap! events-state assoc :event :down)
195 | (go (>! out-chan (merge {:type :down} transducted)))
196 |
197 | (js/setTimeout #(swap! events-state assoc :event nil) timeout-time)
198 | nil))
199 |
200 | ;; We stay in this loop while the mouse move without mousedown
201 | [mouse-move] ([transducted] (do
202 | (go (>! out-chan (merge {:type :move} transducted)))
203 | (recur)))))
204 | (loop [nb-drags 0]
205 | (alt! [mouse-up] ([transducted] (do
206 | (go (>! out-chan (merge {:type :up} transducted)))
207 |
208 | (when (= :down (:event @events-state))
209 | ;; the last event was a :down -> we trigger a click
210 | ;; if we already had a click before it's a double-click
211 | (if (and
212 | (:last-was-a-click @events-state)
213 | (= (:coords transducted) (:last-coords @events-state)))
214 | (go
215 | (swap! events-state assoc :last-was-a-click false)
216 | (>! out-chan (merge {:type :double-click} transducted)))
217 | ;else it's a simple click
218 | (go (swap! events-state assoc :last-was-a-click true)
219 | (>! out-chan (merge {:type :click} transducted))
220 | (js/setTimeout #(swap! events-state assoc :last-was-a-click false) timeout-time))))
221 |
222 | (swap! events-state assoc :event :up)
223 | (swap! events-state assoc :last-coords (:coords transducted))
224 |
225 | nil))
226 | [mouse-move] ([transducted]
227 | (do
228 | (when (> nb-drags 3)
229 | (swap! events-state assoc :last-was-a-click false)
230 | (swap! events-state assoc :event :drag)
231 | (swap! events-state assoc :last-coords (:coords transducted))
232 | (go (>! out-chan (merge {:type :drag} transducted))))
233 | (recur (inc nb-drags))))))
234 | (recur))
235 | out-chan
236 | ))
237 |
238 |
239 | (defn listen-to-canvas
240 | "Listen to a canvas and return a chan of events."
241 | [canvas]
242 | (let [transduct-mouse (map (fn [e]
243 | ;;(.preventDefault e)
244 | ;;(.stopPropagation e)
245 | {:event e
246 | :coords {:x (.-offsetX e)
247 | :y (.-offsetY e)}}))
248 | mousedown-chan (chan (sliding-buffer 1) transduct-mouse)
249 | mouseup-chan (chan (sliding-buffer 1) transduct-mouse)
250 | mousemove-chan (chan (sliding-buffer 1) transduct-mouse)]
251 |
252 | (.addEventListener canvas "mousedown" (fn [e] (put! mousedown-chan e) false))
253 | (.addEventListener canvas "mousemove" (fn [e] (put! mousemove-chan e) false))
254 | (.addEventListener canvas "mouseup" (fn [e] (put! mouseup-chan e) false))
255 |
256 | (listen-to-mouse-events mousedown-chan mouseup-chan mousemove-chan)))
257 |
258 |
259 |
260 |
261 | (defn apply-events-to
262 | [mouse canvas camera raycaster intersect-plane controls state force-worker chan-out]
263 | (let [events-state (atom {})]
264 | (go-loop []
265 | (let [event ( old-node .-selected) false)
291 | (.remove (-> old-node .-mesh) circle)
292 | (set! (-> circle .-visible) true))
293 | (when-not (nil? new-node)
294 | (set! (-> new-node .-selected) true)
295 | (.add (-> new-node .-mesh) circle)
296 | (set! (-> circle .-visible) true))))
297 |
298 |
299 | (defn watch-state
300 | [state watch-id]
301 | (add-watch state watch-id
302 | (fn [id state old-state new-state]
303 | (put-select-circle-on-node old-state new-state)
304 | )))
305 |
--------------------------------------------------------------------------------
/src/gravity/view/graph.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.view.graph
2 | (:require
3 | [gravity.tools :refer [log]]
4 | [gravity.view.node :as node]
5 | [gravity.view.nodeset :as points]
6 | [gravity.view.graph-tools :as tools]
7 | [gravity.view.events-generator :as events]
8 | [gravity.force.proxy :as worker]))
9 |
10 |
11 |
12 |
13 |
14 |
15 | ;; Init view's parameters
16 |
17 |
18 | (defn get-components
19 | "Generate or re-use all the necessary components of the 3D view"
20 | [user-map dev-mode]
21 | (if (:canvas user-map)
22 | (tools/fill-window! (:canvas user-map)))
23 |
24 | (let [webgl-params (:webgl user-map)
25 | width (.-width (:canvas user-map))
26 | height (.-height (:canvas user-map))
27 | camera (new js/THREE.PerspectiveCamera 75 (/ width height) 0.1 100000 )]
28 |
29 | (set! (.-z (.-position camera)) 300)
30 |
31 | { :scene (new js/THREE.Scene)
32 | :width width
33 | :height height
34 | :camera camera
35 | :stats (:stats user-map)
36 | :controls (new js/THREE.OrbitControls camera)
37 | :renderer (new js/THREE.WebGLRenderer #js {:antialias (:antialias webgl-params)
38 | :canvas (:canvas user-map)})
39 | :raycaster (new THREE.Raycaster)
40 | :classifier (:color user-map)
41 | :force-worker (if (:force-worker user-map)
42 | (:force-worker user-map)
43 | (worker/create (:worker-path user-map) (:force user-map)))
44 |
45 | :state (atom {:should-run true})
46 | :first-run (:first-run user-map)}))
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ;; CALLBACKS
55 |
56 |
57 |
58 | (defn- render-callback
59 | "Return a function rendering the context"
60 | [renderer scene camera stats state controls select-circle]
61 | (fn render []
62 |
63 | (.update controls)
64 |
65 | (when-not (nil? stats)
66 | (.begin stats))
67 |
68 | (if (get @state :should-run)
69 | (do
70 | (when-not (nil? select-circle)
71 | (let [x1 (-> select-circle .-rotation .-x)
72 | y1 (-> select-circle .-rotation .-y)
73 | x2 (+ x1 0.01)
74 | y2 (+ y1 0.1)]
75 | (.set (-> select-circle .-rotation) x2 y2 0)))
76 | (.requestAnimationFrame js/window render)
77 | (.render renderer scene camera)))
78 |
79 | (when-not (nil? stats)
80 | (.end stats))))
81 |
82 |
83 |
84 |
85 | (defn- start-callback!
86 | "Return a closure affecting the application state atom
87 | and triggering the render function once"
88 | [state render]
89 | (fn []
90 | (when-not (:should-run @state)
91 | (swap! state assoc :should-run true)
92 | (render))
93 | nil))
94 |
95 |
96 |
97 | (defn- stop-callback!
98 | "Return a closure affecting the application state atom"
99 | [state]
100 | (fn []
101 | (swap! state assoc :should-run false) nil))
102 |
103 | (defn- resume-force-callback
104 | "Send a resume event to the force worker"
105 | [force-worker]
106 | (fn []
107 | (worker/send force-worker "resume")))
108 |
109 |
110 |
111 |
112 |
113 | (defn- set-links
114 | "Remove the old links and add the new ones"
115 | [state scene links]
116 | (let [old-links (:links-set @state)
117 | nodes (:nodes @state)
118 | new-links (points/create-links nodes links)]
119 | (.remove scene old-links)
120 | (.add scene new-links)
121 | (swap! state assoc :links links)
122 | (swap! state assoc :links-set new-links)
123 | links))
124 |
125 | (defn- set-nodes
126 | "Remove the old nodes and add the new ones"
127 | [state scene nodes]
128 | (let [classifier (:classifier @state)
129 | old-nodes (:nodes @state)
130 | {nodes :nodes
131 | meshes :meshes} (points/prepare-nodes nodes classifier)]
132 | (doseq [old old-nodes]
133 | (.remove scene (-> old .-mesh)))
134 | (doseq [new-node meshes]
135 | (.add scene new-node))
136 | (swap! state assoc :nodes nodes)
137 | (swap! state assoc :meshes meshes)
138 | (set-links state scene [])
139 | nodes))
140 |
141 |
142 |
143 | (defn- set-nodes-callback
144 | "Allow the user to replace nodes with a new set of nodes"
145 | [state scene]
146 | (fn [nodes]
147 | (if (nil? nodes)
148 | (:nodes @state)
149 | (set-nodes state scene nodes))))
150 |
151 | (defn- set-links-callback
152 | [state scene]
153 | (fn [links]
154 | (if (nil? links)
155 | (:links @state)
156 | (set-links state scene links))))
157 |
158 |
159 |
160 |
161 | (defn- update-force-callback
162 | [state force-worker]
163 | (fn []
164 | (let [nodes (:nodes @state)
165 | links (:links @state)]
166 | (worker/send force-worker "set-nodes" (count nodes))
167 | (worker/send force-worker "set-links" links)
168 | ;;(worker/send force-worker "start")
169 | )))
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | ;; USE ALL THE ABOVE
180 |
181 |
182 | (defn init-force
183 | [force-worker dev-mode first-run]
184 | (when (and (not first-run) dev-mode)
185 | (worker/send force-worker "tick")))
186 |
187 |
188 | (defn create
189 | "Initialise a context in the specified element id"
190 | [user-map chan-out dev-mode]
191 | (let [{ first-run :first-run
192 | scene :scene
193 | width :width
194 | height :height
195 | camera :camera
196 | stats :stats
197 | controls :controls
198 | renderer :renderer
199 | raycaster :raycaster
200 | classifier :classifier
201 | force-worker :force-worker
202 | state :state} (get-components user-map dev-mode)
203 |
204 | canvas (if-not (nil? (:canvas user-map))
205 | (:canvas user-map)
206 | (.-domElement renderer))
207 |
208 | select-circle (tools/get-circle)
209 | intersect-plane (tools/get-intersect-plane)
210 | ;;data-test (demo/get-demo-graph)
211 | ;;{nodes :nodes
212 | ;; meshes :meshes} (points/prepare-nodes (.-nodes data-test) classifier)
213 |
214 | ;;links (.-links data-test)
215 | ;;nodeset (points/create nodes classifier)
216 | ;;links-set (points/create-links nodes links)
217 | render (render-callback renderer scene camera stats state controls select-circle)]
218 |
219 |
220 | ;; (swap! state assoc :nodes nodes)
221 | ;; (swap! state assoc :meshes meshes)
222 | ;; (swap! state assoc :links links)
223 | ;; (swap! state assoc :links-set links-set)
224 | (swap! state assoc :classifier classifier)
225 | (swap! state assoc :select-circle select-circle)
226 |
227 |
228 | ;; renderer
229 | (.setSize renderer width height)
230 | (.setClearColor renderer 0x404040)
231 |
232 | ;;shadows
233 | (when (:shadows (:webgl user-map))
234 | (set! (-> renderer .-shadowMap .-enabled) true)
235 | (set! (-> renderer .-shadowMap .-type) js/THREE.PCFSoftShadowMap))
236 |
237 |
238 |
239 | (worker/listen force-worker (fn [event]
240 | (let [message (.-data event)
241 | type (.-type message)
242 | data (.-data message)]
243 | (case type
244 | "ready" (do
245 | (init-force force-worker dev-mode first-run)
246 | (events/notify-user-ready chan-out))
247 | "nodes-positions" (let [state @state]
248 | (points/update-positions! (:nodes state) data)
249 | ;;(points/update nodeset)
250 | (points/update-geometry (:links-set state))
251 | )))))
252 |
253 |
254 |
255 |
256 | ;; if it's not the first time in dev mode
257 | (when (and (not first-run) dev-mode)
258 | (tools/fill-window! canvas)
259 | (.removeEventListener canvas "mousemove")
260 | (.removeEventListener canvas "click")
261 | (events/notify-user-ready chan-out))
262 |
263 |
264 | ;;(worker/send force-worker "precompute" 50)
265 |
266 | (.add scene select-circle)
267 | (set! (-> select-circle .-visible) false)
268 |
269 | (.add scene intersect-plane)
270 |
271 | (.addEventListener js/window "resize" (events/onWindowResize canvas renderer camera))
272 |
273 |
274 |
275 | (let [;mouse (events/listen-to-canvas canvas)
276 | mouse (events/listen-to-canvas js/window.document)]
277 | (events/apply-events-to mouse canvas camera raycaster intersect-plane controls state force-worker chan-out))
278 |
279 |
280 |
281 |
282 |
283 |
284 | (let [webgl-params (:webgl user-map)
285 | background? (:background webgl-params)
286 | lights? (:lights webgl-params)]
287 | ;; add background
288 | (when background?
289 | (.add scene (tools/get-background)))
290 |
291 | ;; add lights
292 |
293 | (doseq [light (tools/get-lights lights?)]
294 | (.add scene light)))
295 |
296 |
297 | (events/watch-state state :main-watcher)
298 |
299 |
300 | (render)
301 |
302 | ;; return closures to user
303 |
304 | {:start (start-callback! state render)
305 | :stop (stop-callback! state)
306 | :resume (resume-force-callback force-worker)
307 | :tick (fn [] (worker/send force-worker "tick"))
308 | :canvas (.-domElement renderer)
309 | :stats stats
310 | :nodes (set-nodes-callback state scene)
311 | :links (set-links-callback state scene)
312 | :updateForce (update-force-callback state force-worker)
313 |
314 | :force {:size (fn [array] (worker/send force-worker "size" array))
315 | :linkStrength (fn [val] (worker/send force-worker "linkStrength" (worker/serialize val)))
316 | :friction (fn [val] (worker/send force-worker "friction" val))
317 | :linkDistance (fn [val] (worker/send force-worker "linkDistance" (worker/serialize val)))
318 | :charge (fn [val] (worker/send force-worker "charge" (worker/serialize val)))
319 | :gravity (fn [val] (worker/send force-worker "gravity" val))
320 | :theta (fn [val] (worker/send force-worker "theta" val))
321 | :alpha (fn [val] (worker/send force-worker "alpha" val))}
322 |
323 | :selectNode (fn [node]
324 | (swap! state assoc :selected node)
325 | (:selected @state))
326 | :pinNode (fn [node]
327 | (let [circle (tools/get-circle)
328 | mesh (-> node .-mesh)]
329 | (set! (-> mesh .-circle) circle)
330 | (.add mesh circle))
331 | (worker/send force-worker "pin" {:index (-> node .-index)}))
332 | :unpinNode (fn [node]
333 | (tools/remove-children (-> node .-mesh))
334 | (.remove (-> node .-mesh) (-> node .-mesh .-circle))
335 | (worker/send force-worker "unpin" {:index (-> node .-index)}))
336 | :camera camera
337 | }))
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
--------------------------------------------------------------------------------
/src/gravity/view/graph_tools.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.view.graph-tools
2 | "Contain tools like selection animation, lights, background, etc…"
3 |
4 | (:require [gravity.tools :refer [log]]))
5 |
6 |
7 |
8 | (defn fill-window!
9 | "Resize a canvas to make it fill the window"
10 | [canvas]
11 | (set! (-> canvas .-style .-width) "100%")
12 | (set! (-> canvas .-style .-height) "100%")
13 | (let [width (.-offsetWidth canvas)
14 | height (.-offsetHeight canvas)]
15 | (set! (.-width canvas) width)
16 | (set! (.-height canvas) height))
17 | nil)
18 |
19 |
20 |
21 | (defn make-stats
22 | "Create a stat view to monitor performances"
23 | []
24 | (let [stats (new js/Stats)
25 | style (-> stats
26 | (.-domElement)
27 | (.-style))]
28 | (set! (.-position style) "absolute")
29 | (set! (.-left style) "0px")
30 | (set! (.-top style) "0px")
31 | stats))
32 |
33 | (defn make-fake-stats
34 | "Create a fake object that will be returned in place of stats
35 | Used in prod mode"
36 | []
37 | #js {:domElement nil})
38 |
39 |
40 |
41 |
42 | (defn- get-background
43 | "Generate a gray sphere as a background"
44 | []
45 | (let [material (new js/THREE.MeshLambertMaterial #js {:color 0xa0a0a0
46 | ;:ambiant 0xffffff
47 | :side 1})
48 | geometry (new js/THREE.SphereGeometry 20 20 20)
49 | background (new js/THREE.Mesh geometry material)]
50 | (.set (.-scale background) 100 100 100)
51 | (set! (.-receiveShadow background) true)
52 | background))
53 |
54 |
55 |
56 | (defn- get-flat-light
57 | "Generate an ambient light"
58 | []
59 | (new js/THREE.AmbientLight 0xffffff))
60 |
61 |
62 | (defn- get-spot-lights
63 | []
64 | (let [color (new js/THREE.Color 0xffffff)
65 | strength 0.8
66 | shadow-map 2048
67 | positions [[-1000 1000 1000]
68 | [1000 1000 1000]
69 | [-1000 -1000 1000]
70 | [1000 -1000 1000]
71 |
72 | [1000 1000 -1000]]
73 | lights (map (fn [pos]
74 | (let [light (new js/THREE.SpotLight color strength)
75 | [x y z] pos]
76 | (.set (.-position light) x y z)
77 | (set! (.-shadowCameraFar light) 4000)
78 |
79 | light))
80 | positions)]
81 | (let [main-light (first lights)]
82 | (set! (.-castShadow main-light) true)
83 | (set! (.-shadowCameraVisible main-light) false))
84 |
85 | lights))
86 |
87 |
88 | (defn get-lights
89 | "Make light(s) for the scene. If spots is true, generate directional lights"
90 | [spots]
91 | (if-not spots
92 | (conj [] (get-flat-light))
93 | (get-spot-lights)))
94 |
95 |
96 |
97 | (defn get-circle
98 | "Return a circle meant to be placed and animated on a node."
99 | ([]
100 | (get-circle 32 15))
101 |
102 | ([nb-segments radius]
103 | (let [material (new js/THREE.LineBasicMaterial #js {:color 0xff0000})
104 | geometry (new js/THREE.Geometry)]
105 | (doseq [i (range nb-segments)]
106 | (let [theta (-> i
107 | (/ nb-segments)
108 | (* Math/PI)
109 | (* 2))
110 | cos (-> (Math/cos theta)
111 | (* radius))
112 | sin (-> (Math/sin theta)
113 | (* radius))
114 | vect (new js/THREE.Vector3 cos sin 0)]
115 | (.push (-> geometry .-vertices) vect)))
116 | ;; close circle
117 | (.push (-> geometry .-vertices) (aget (-> geometry .-vertices) 0))
118 | ;ret
119 | (new js/THREE.Line geometry material))))
120 |
121 |
122 |
123 |
124 |
125 | (defn get-intersect-plane
126 | "Return a big plane filling the sphere. Used to drag nodes"
127 | []
128 | (let [side 4000
129 | material (new js/THREE.MeshBasicMaterial #js {:wireframe true
130 | :visible false})
131 | geometry (new js/THREE.PlaneGeometry side side 1 1)
132 | mesh (new js/THREE.Mesh geometry material)]
133 | mesh))
134 |
135 |
136 |
137 |
138 |
139 | (defn remove-children
140 | [object3D]
141 | (loop [len (dec (-> object3D .-children .-length))
142 | i (range 0 len)]
143 |
144 | (.remove object3D (aget (-> object3D .-children) 0))
145 |
146 | (when (< i len)
147 | (recur len (inc i))))
148 | nil)
149 |
--------------------------------------------------------------------------------
/src/gravity/view/node.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.view.node)
2 |
3 |
4 |
5 | (defn- get-color ;; TODO
6 | "Give a color for a given node"
7 | [node classifier]
8 | (let [key (.-group node)]
9 | (new js/THREE.Color (classifier key))))
10 |
11 | (def get-unique-color
12 | (memoize get-color))
13 |
14 |
15 |
16 | (defn get-rand-pos
17 | "Give a random position between -extent and +extent"
18 | [extent]
19 | (-> (* 2 extent)
20 | (rand)
21 | (- extent)))
22 |
23 |
24 |
25 | (defn generate-geometry
26 | "Generate a generic geometry"
27 | []
28 | (new js/THREE.SphereGeometry 10 10 10))
29 |
30 |
31 | (def get-unique-geometry
32 | (memoize generate-geometry))
33 |
34 | (defn generate-material
35 | "Generate a generic material"
36 | [color-key]
37 | (new js/THREE.MeshLambertMaterial (clj->js {:color (new js/THREE.Color color-key)
38 | :visible true
39 | :wireframe false})))
40 |
41 |
42 | (def get-unique-material
43 | (memoize generate-material))
44 |
45 |
46 | (defn generate-collider
47 | "create and return a new node mesh used for collisions"
48 | [node classifier]
49 | (let [geometry (get-unique-geometry)
50 | material (get-unique-material (get-unique-color node classifier))
51 | sphere (new js/THREE.Mesh geometry material)]
52 | (.set (.-scale sphere) 0.45 0.45 0.45)
53 | sphere))
54 |
55 |
56 |
57 | (defn create
58 | "Return a cloned node with a random position and a collider object"
59 | [node classifier index]
60 | (let [ext 2000
61 | node (.clone js/goog.object node)
62 | collider (generate-collider node classifier)
63 | position (new js/THREE.Vector3 (get-rand-pos ext) (get-rand-pos ext) 0)]
64 | (set! (.-index node) index)
65 | (set! (.-position node) position)
66 | (set! (.-mesh node) collider)
67 | (set! (.-castShadow collider) true)
68 | (set! (.-node collider) node)
69 | (set! (.-selected node) false)
70 | node))
71 |
72 |
--------------------------------------------------------------------------------
/src/gravity/view/nodeset.cljs:
--------------------------------------------------------------------------------
1 | (ns gravity.view.nodeset
2 | (:refer-clojure :exclude [update])
3 | (:require [gravity.tools :refer [log]]
4 | [gravity.view.node :as node]))
5 |
6 |
7 | (declare get-unique-color)
8 | (declare get-shader-material)
9 |
10 |
11 |
12 |
13 | (defn create-links
14 | "Given a js-array of nodes and a js array of links, will return a THREE.LineSegments"
15 | [nodes links]
16 | (let [geometry (new js/THREE.Geometry)
17 | vertices (.-vertices geometry)
18 | material (new js/THREE.LineBasicMaterial #js {"color" 0xfafafa})
19 | system (new js/THREE.LineSegments geometry material)
20 | size (dec (.-length links))]
21 | (doseq [link links]
22 | (let [source (get nodes (.-source link))
23 | target (get nodes (.-target link))]
24 | (.push vertices (.-position source))
25 | (.push vertices (.-position target))))
26 | (set! (.-verticesNeedUpdate geometry) true)
27 | (set! (.-castShadow system) true)
28 | system))
29 |
30 | (defn prepare-nodes
31 | "Create a array of cloned nodes containing a position and a collider object.
32 | Return a map {nodes[] colliders[]} meant to be destructured.
33 | The nodes and the colliders are in the same order and share the same position Vector3."
34 | [nodes classifier]
35 | (let [counter (atom 0)
36 | pairs (map (fn [node]
37 | (let [index @counter
38 | prepared-node (node/create node classifier index)
39 | mesh (.-mesh prepared-node)]
40 | (swap! counter inc)
41 | [prepared-node mesh]))
42 | nodes)]
43 | {:nodes (clj->js (mapv first pairs))
44 | :meshes (clj->js (mapv last pairs))}))
45 |
46 |
47 | (defn update-geometry
48 | "Update a nodeset Points geometry or a LineSegments geometry"
49 | [geom-based-item]
50 | (set! (.-verticesNeedUpdate (.-geometry geom-based-item)) true)
51 | geom-based-item)
52 |
53 |
54 | (defn update-positions!
55 | "Update the nodes' positions according to the raw array given by the force"
56 | [nodes positions]
57 | (let [size (dec (.-length nodes))]
58 | (loop [i 0]
59 | (let [j (* i 3)
60 | node (get nodes i)
61 | x (aget positions j)
62 | y (aget positions (+ j 1))
63 | z (aget positions (+ j 2))]
64 | (.set (-> node .-position) x y z)
65 | (.set (-> node .-mesh .-position) x y z)
66 | (when (< i size)
67 | (recur (inc i)))))))
68 |
69 |
70 | ; Colors & shape
71 |
72 |
73 | (defn get-color ;; TODO
74 | "Give a color for a given node"
75 | [classifier node]
76 | (let [key (.-group node)]
77 | (new js/THREE.Color (classifier key))))
78 |
79 | (def get-unique-color
80 | (memoize get-color))
81 |
82 |
--------------------------------------------------------------------------------