/
three.cljc
376 lines (293 loc) · 12.8 KB
/
three.cljc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
(ns app.three
#?(:cljs (:require-macros [app.three :refer [gen_factory]]))
(:require
[hyperfiddle.electric :as e]
#?(:cljs ["three" :as three])
[contrib.missionary-contrib :as mx]
[missionary.core :as m]
[hyperfiddle.electric-dom2 :as dom])
(:import (hyperfiddle.electric Pending)))
#?(:clj (defn flatten-props
([m] (flatten-props m []))
([m p]
(if (map? m)
(mapcat
(fn [[k v]]
(flatten-props v (conj p k))) m)
[[p m]]))))
#?(:cljs (defn interop-js
([^js cls] #?(:cljs (new cls)))
([^js cls a#] #?(:cljs (new cls a#)))
([^js cls a# b#] #?(:cljs (new cls a# b#)))
([^js cls a# b# c#] #?(:cljs (new cls a# b# c#)))
([^js cls a# b# c# d#] #?(:cljs (new cls a# b# c# d#)))
([^js cls a# b# c# d# e#] #?(:cljs (new cls a# b# c# d# e#)))
([^js cls a# b# c# d# e# f#] #?(:cljs (new cls a# b# c# d# e# f#)))
([^js cls a# b# c# d# e# f# g#] #?(:cljs (new cls a# b# c# d# e# f# g#)))
([^js cls a# b# c# d# e# f# g# h#] #?(:cljs (new cls a# b# c# d# e# f# g# h#)))
([^js cls a# b# c# d# e# f# g# h# i#] #?(:cljs (new cls a# b# c# d# e# f# g# h# i#)))
([^js cls a# b# c# d# e# f# g# h# i# j#] #?(:cljs (new cls a# b# c# d# e# f# g# h# i# j#)))
([^js cls a# b# c# d# e# f# g# h# i# j# k#] #?(:cljs (new cls a# b# c# d# e# f# g# h# i# j# k#)))))
(e/def three_obj)
(e/def rerender-flag)
(e/def view-port-ratio)
(defmacro mark-render! []
`(reset! rerender-flag true))
#?(:cljs(defn -setlistener [^js obj val]
(set! (.-listeners obj) val)))
(defmacro bare-obj [cls unmount-fns body]
(let [args (first body)
body-args (rest body)
s (symbol cls)]
`(do
(let [obj# (apply interop-js ~s ~args)]
(-setlistener obj# (atom {}))
(binding [three_obj obj#]
(e/on-unmount #(mark-render!))
~@body-args
~@unmount-fns
obj#)))))
#?(:cljs (defn -dispose [^js obj]
(.dispose obj)))
(defmacro disposable-obj [cls body]
`(do
(bare-obj ~cls
[(e/on-unmount #(-dispose three_obj))]
~body)))
#?(:cljs (defn -removeFromParent [^js obj]
(.removeFromParent obj)))
(defmacro scene-obj [cls body]
`(let [obj# (disposable-obj ~cls ~body)]
(e/on-unmount #(-removeFromParent obj#))
(.add three_obj obj#)
(mark-render!)
obj#))
(defmacro set-prop-fn [path]
`(fn [val#]
(set! (.. three_obj ~@path) val#)
(mark-render!)))
(defmacro unmount-prop [fn]
`(new (m/observe (fn [!#] (!# nil) ~fn))))
(defmacro props [m]
`(do ~@(map (fn [[k v]]
(let [path (map #(symbol (str "-" (name %))) k)]
`(let [org-val# (.. three_obj ~@path)]
((set-prop-fn ~path) ~v)
(unmount-prop #((set-prop-fn ~path) org-val#))))) (sort-by first (flatten-props m)))))
(defn invoke-setter [rerender-flag obj setfn & args]
(apply setfn obj args)
(reset! rerender-flag true))
(defmacro setter [undo setfn & args]
`(do
(invoke-setter rerender-flag three_obj ~setfn ~@args)
(e/on-unmount #(~undo three_obj))
three_obj))
(defmacro gen_factory [mname kw macro]
(let [full-macro (symbol (resolve macro))]
`(do
(defmacro ~mname [& body#]
(list '~full-macro ~kw body#)))
))
(comment
(macroexpand '(gen_factory WebGLRenderer :three/WebGLRenderer disposable-obj)))
(gen_factory WebGLRenderer :three/WebGLRenderer disposable-obj)
(gen_factory PerspectiveCamera :three/PerspectiveCamera disposable-obj)
(gen_factory Scene :three/Scene disposable-obj)
(gen_factory Mesh :three/Mesh scene-obj)
(gen_factory Group :three/Group scene-obj)
;geometries
(gen_factory BoxGeometry :three/BoxGeometry disposable-obj)
(gen_factory CapsuleGeometry :three/CapsuleGeometry disposable-obj)
(gen_factory CircleGeometry :three/CircleGeometry disposable-obj)
(gen_factory ConeGeometry :three/ConeGeometry disposable-obj)
(gen_factory CylinderGeometry :three/CylinderGeometry disposable-obj)
(gen_factory DodecahedronGeometry :three/DodecahedronGeometry disposable-obj)
(gen_factory EdgesGeometry :three/EdgesGeometry disposable-obj)
(gen_factory ExtrudeGeometry :three/ExtrudeGeometry disposable-obj)
;lights
(gen_factory AmbientLight :three/AmbientLight scene-obj)
(gen_factory AmbientLightProbe :three/AmbientLightProbe scene-obj)
(gen_factory DirectionalLight :three/DirectionalLight scene-obj)
(gen_factory HemisphereLight :three/HemisphereLight scene-obj)
(gen_factory HemisphereLightProbe :three/HemisphereLightProbe scene-obj)
(gen_factory PointLight :three/PointLight scene-obj)
(gen_factory RectAreaLightHelper :three/PRectAreaLightHelper scene-obj)
(gen_factory SpotLight :three/SpotLight scene-obj)
;materials
(gen_factory MeshLambertMaterial :three/MeshLambertMaterial disposable-obj)
(gen_factory MeshBasicMaterial :three/MeshBasicMaterial disposable-obj)
(gen_factory MeshStandardMaterial :three/MeshStandardMaterial disposable-obj)
#?(:cljs (defn set_camera [^js camera pos target]
(let [{px :x py :y pz :z} pos
{tx :x ty :y tz :z} target]
(set! (.. camera -position -x) px)
(set! (.. camera -position -y) py)
(set! (.. camera -position -z) pz)
(.lookAt camera tx ty tz)
(.updateProjectionMatrix camera))))
#?(:cljs (defn reset_camera [^js camera]
(set! (.-position camera) (three/Vector3. 0 0 0))
(.lookAt camera 0 0 0)
(.updateProjectionMatrix camera)))
(defn -control-render [mat s]
(reset! s true))
(e/defn cam_mat [controls]
(if (= "visible" e/dom-visibility-state)
(new (m/sample #(do
(.update controls)
[(vec (.. controls -object -position)) (vec (.. controls -object -quaternion))]) e/<clock))
(throw (Pending.)))) ;
(defmacro control []
`(when (= "visible" e/dom-visibility-state)
(let [mat# (new cam_mat three_obj)]
(-control-render mat# rerender-flag))))
(gen_factory OrbitControls :orbitcontrols/OrbitControls disposable-obj)
#?(:cljs (defn -render [^js renderer ^js scene ^js camera tick !rerender]
(when @!rerender
(print "render")
(reset! !rerender false)
(.updateProjectionMatrix camera)
(.render renderer scene camera))))
(defn -size [rect] [(.-width rect) (.-height rect)])
(defn size> [node]
#?(:cljs (->> (m/observe (fn [!]
(! (-> node .getBoundingClientRect))
(let [resize-observer (js/ResizeObserver. (fn [[nd] _] (! (-> nd .-target .getBoundingClientRect))))]
(.observe resize-observer node)
#(.disconnect resize-observer))))
(m/relieve {}))))
#?(:cljs (defn node-resized [flag]
(fn [^js renderer w h]
(.setSize renderer w h)
(reset! flag true))))
#?(:cljs (defn dom-listener [^js obj typ f]
(swap! (.-listeners obj) update typ #(if (nil? %) #{f} (conj % f)))
#(swap! (.-listeners obj) update typ (fn [x] (disj x f)))))
#?(:cljs (defn listen> ; we intend to replace this in UI5 workstream
([^js node event-type] (listen> node event-type identity))
([^js node event-type keep-fn!]
(m/relieve {}
(m/observe (fn [!]
(dom-listener node event-type #(when-some [v (keep-fn! %)]
(! v)))))))))
(defmacro on!
"Call the `callback` clojure function on event.
(on! \"click\" (fn [event] ...)) "
([event-name callback] `(on! three_obj ~event-name ~callback))
([dom-node event-name callback]
`(new (->> (listen> ~dom-node ~event-name ~callback)
(m/reductions {} nil)))))
(defmacro on
"Run the given electric function on event.
(on \"click\" (e/fn [event] ...))"
;; TODO add support of event options (see `event*`)
;(^:deprecated [typ] `(new Event ~typ false)) ; use `on!` for local side effects
([typ F] `(on three_obj ~typ ~F))
([node typ F] `(binding [three_obj ~node]
(let [[state# v#] (e/for-event-pending-switch [e# (listen> ~node ~typ)] (new ~F e#))]
(case state#
(::e/init ::e/ok) v# ; could be `nil`, for backward compat we keep it
(::e/pending) (throw (Pending.))
(::e/failed) (throw v#))))))
(defn -intersected [x y scene camera]
#?(:cljs
(let [pointer (three/Vector2.)
caster (three/Raycaster.)]
(set! (.-x pointer) x)
(set! (.-y pointer) y)
(.setFromCamera caster pointer camera)
(when-let [i (first (seq (.intersectObjects caster (.-children scene) true)))]
(js->clj i)))))
(defn -pointer [rect [x y]]
(let [[width height] (-size rect)
dx (- (inc x) (.-left rect))
dy (- (inc y) (.-top rect))
px (dec (* 2 (/ dx width)))
py (- (dec (* 2 (/ dy height))))]
[px py]))
(defn -call-event-stack [{obj :obj e :e data :data } typ]
(let [listeners (deref (.. obj -listeners))]
(dorun (map #(% {:obj obj :e e :data data}) (listeners typ)))
(when-let [parent (.-parent obj)]
(-call-event-stack {:obj parent :e e :data data} typ))))
(defn -on-event [e rect scene camera typ]
(let [[px py] (-pointer rect [(.-pageX e) (.-pageY e)])
obj (-intersected px py scene camera)]
(when obj
(-call-event-stack {:obj (obj "object") :e e :data obj} typ))))
(defn intersected [v x y scene camera]
#?(:cljs
(let [intersection (-intersected x y scene camera)
i-obj (get intersection "object")
last-intersection @v
l-obj (get last-intersection "object")]
(do
(vreset! v intersection)
(if intersection
(if (== i-obj l-obj)
{"pointermove" intersection}
(if last-intersection
{"pointermove" intersection "pointerout" last-intersection "pointerenter" intersection }
{"pointermove" intersection "pointerenter" intersection}))
(if last-intersection
{"pointerout" last-intersection}
{}))))))
(defn -on-event2 []
(let [v (volatile! nil)]
(fn [e rect scene camera typ]
(let [[px py] (-pointer rect [(.-pageX e) (.-pageY e)])
obj (intersected v px py scene camera)]
(dorun (map (fn [[k v]]
(-call-event-stack {:obj (v "object") :e e :data v} k)) obj)
)))))
#?(:cljs (e/defn init-callbacksystem [^js rect ^js scene ^js camera]
(let [f (-on-event2)]
(dom/on! "pointermove" #(f % rect scene camera "pointermove")))
(dom/on! "click" #(-on-event % rect scene camera "click"))))
(e/defn Hovered? "Returns whether this DOM `node` is hovered over."
[]
(->> (mx/mix
(listen> three_obj "pointerenter" (constantly true))
(listen> three_obj "pointerout" (constantly false)))
(m/reductions {} false)
(m/relieve {})
new))
#?(:cljs (defn -setPixelRatio [^js obj]
(.setPixelRatio obj (.-devicePixelRatio js/window))))
(defmacro canvas [renderer camera scene]
`(let [!rerender# (atom true)]
(binding [rerender-flag !rerender#]
(let [renderer# ~renderer
node# (.-domElement renderer#)
rect# (new (size> dom/node))
[width# height#] (-size rect#)]
(.appendChild dom/node node#)
(binding [view-port-ratio (/ width# height#)]
(let [camera# ~camera
scene# ~scene
tick# (e/client e/system-time-ms)]
(new init-callbacksystem rect# scene# camera#)
(binding [dom/node node#]
((node-resized rerender-flag) renderer# width# height#)
(-setPixelRatio renderer#)
(-render renderer# scene# camera# tick# !rerender#)
(e/on-unmount #(do
(some-> (.-parentNode node#) (.removeChild node#)))))))))))
(comment
(macroexpand `(three_js))
)
(comment
(use 'clojure.walk)
(defn a []
(let [e (fn [] 42)]
(e)))
(macroexpand `(canvas a b c))
(macroexpand-all `(props {:a 1 :b {:c 3}}))
(namespace :three/foo)
(macroexpand-all `(th2/Scene (th2/Mesh (th2/BoxGeometry)
(th2/MeshBasicMaterial
(th2/props {:color {:r 0.5 :g 0.0 :b (/ state 10)}})))))
(macroexpand-all
`(PerspectiveCamera 75 1 0.1 1000
(props {:position (vec3 0 0 2)})))
1)