Testing out JmonkeyEngine to make a game in Scala with Akka Actors within a pure FP layer
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

680 lines
19 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. package wow.doge.mygame
  2. import scala.jdk.CollectionConverters._
  3. import scala.reflect.ClassTag
  4. import akka.actor.typed.ActorRef
  5. import akka.actor.typed.Scheduler
  6. import akka.util.Timeout
  7. import com.jayfella.jme.jfx.JavaFxUI
  8. import com.jme3.app.Application
  9. import com.jme3.app.SimpleApplication
  10. import com.jme3.app.state.AppState
  11. import com.jme3.app.state.AppStateManager
  12. import com.jme3.asset.AssetLocator
  13. import com.jme3.asset.AssetManager
  14. import com.jme3.bullet.BulletAppState
  15. import com.jme3.bullet.PhysicsSpace
  16. import com.jme3.bullet.PhysicsTickListener
  17. import com.jme3.bullet.collision.PhysicsCollisionEvent
  18. import com.jme3.bullet.collision.PhysicsCollisionListener
  19. import com.jme3.bullet.control.BetterCharacterControl
  20. import com.jme3.input.Action
  21. import com.jme3.input.InputManager
  22. import com.jme3.input.controls.ActionListener
  23. import com.jme3.input.controls.AnalogListener
  24. import com.jme3.input.controls.InputListener
  25. import com.jme3.input.controls.Trigger
  26. import com.jme3.light.Light
  27. import com.jme3.math.Vector3f
  28. import com.jme3.scene.CameraNode
  29. import com.jme3.scene.Geometry
  30. import com.jme3.scene.Node
  31. import com.jme3.scene.SceneGraphVisitor
  32. import com.jme3.scene.Spatial
  33. import com.jme3.scene.control.CameraControl.ControlDirection
  34. import com.jme3.scene.control.Control
  35. import com.simsilica.es.EntityComponent
  36. import com.simsilica.es.EntityData
  37. import com.simsilica.es.EntityId
  38. import enumeratum._
  39. import monix.bio.Task
  40. import monix.bio.UIO
  41. import monix.execution.Ack
  42. import monix.execution.Ack.Continue
  43. import monix.execution.Ack.Stop
  44. import monix.execution.Cancelable
  45. import monix.execution.cancelables.SingleAssignCancelable
  46. import monix.reactive.Observable
  47. import monix.reactive.OverflowStrategy
  48. import monix.reactive.observers.Subscriber
  49. import wow.doge.mygame.math.ImVector3f
  50. import wow.doge.mygame.state.MyBaseState
  51. import com.jme3.material.Material
  52. final case class ActionEvent(binding: Action, value: Boolean, tpf: Float)
  53. final case class EnumActionEvent[T <: EnumEntry](
  54. binding: T,
  55. value: Boolean,
  56. tpf: Float
  57. )
  58. final case class AnalogEvent(binding: Action, value: Float, tpf: Float)
  59. final case class EnumAnalogEvent[T <: EnumEntry](
  60. binding: T,
  61. value: Float,
  62. tpf: Float
  63. )
  64. final class PrePhysicsTickEvent(val space: PhysicsSpace) extends AnyVal
  65. final class PhysicsTickEvent(val space: PhysicsSpace) extends AnyVal
  66. package object implicits {
  67. type PrePhysicsTickEvent = PhysicsTickEvent
  68. type PhysicsTickObservable =
  69. Observable[Either[PrePhysicsTickEvent, PhysicsTickEvent]]
  70. implicit final class JMEAppExt(private val app: Application) extends AnyVal {
  71. def enqueueR(cb: () => Unit) =
  72. app.enqueue(new Runnable {
  73. override def run() = cb()
  74. })
  75. }
  76. implicit final class StateManagerExt(private val asm: AppStateManager)
  77. extends AnyVal {
  78. def state[S <: AppState]()(implicit c: ClassTag[S]): S =
  79. asm.getState(c.runtimeClass.asInstanceOf[Class[S]])
  80. // def appStates = asm.getStates()
  81. }
  82. implicit final class SimpleApplicationExt[T <: SimpleApplication](
  83. private val sa: T
  84. ) extends AnyVal {
  85. def stateManager: AppStateManager = sa.getStateManager()
  86. def inputManager: InputManager = sa.getInputManager()
  87. def assetManager: AssetManager = sa.getAssetManager()
  88. def guiNode = sa.getGuiNode()
  89. def flyCam = Option(sa.getFlyByCamera())
  90. def camera = sa.getCamera()
  91. def viewPort = sa.getViewPort()
  92. def rootNode = sa.getRootNode()
  93. def observableTick: Observable[Float] =
  94. Observable.create(OverflowStrategy.Unbounded) { sub =>
  95. val c = SingleAssignCancelable()
  96. val as = new MyBaseState {
  97. override def init(): Unit = {}
  98. override def update(tpf: Float) = {
  99. if (sub.onNext(tpf) == Ack.Stop)
  100. c.cancel()
  101. }
  102. override def stop(): Unit = {}
  103. override def onEnable() = {}
  104. override def onDisable() = {}
  105. }
  106. sa.stateManager.attach(as)
  107. c := Cancelable(() => sa.stateManager.detach(as))
  108. c
  109. }
  110. }
  111. implicit final class AssetManagerExt(private val am: AssetManager)
  112. extends AnyVal {
  113. def registerLocator(
  114. assetPath: os.RelPath,
  115. locator: Class[_ <: AssetLocator]
  116. ): Unit = {
  117. am.registerLocator(assetPath.toString(), locator)
  118. }
  119. def loadModel(assetPath: os.RelPath): Spatial = {
  120. am.loadModel(assetPath.toString())
  121. }
  122. }
  123. implicit final class BulletAppStateExt(private val bas: BulletAppState)
  124. extends AnyVal {
  125. def physicsSpace = bas.getPhysicsSpace()
  126. def speed = bas.getSpeed()
  127. }
  128. implicit final class BetterCharacterControlExt(
  129. private val bcc: BetterCharacterControl
  130. ) {
  131. def withJumpForce(force: ImVector3f) = {
  132. bcc.setJumpForce(force.mutable)
  133. bcc
  134. }
  135. }
  136. implicit final class SpatialExt[T <: Spatial](private val spat: T)
  137. extends AnyVal {
  138. // def asRef = Ref[Task].of(spat)
  139. }
  140. implicit final class NodeExt[T <: Node](private val n: T) extends AnyVal {
  141. /**
  142. * Attaches the given child
  143. *
  144. * @param s
  145. * @return
  146. */
  147. def withChild(s: Spatial): Node = {
  148. n.attachChild(s)
  149. n
  150. }
  151. /**
  152. * @return Gets the list of children as a monix observable
  153. */
  154. def observableChildren =
  155. Observable.fromIterable(n.getChildren().asScala)
  156. /**
  157. * @return A copy of the list of children of this node as a lazy list
  158. */
  159. def children = LazyList.from(n.getChildren().asScala)
  160. /**
  161. * Attach given children
  162. *
  163. * @param lst
  164. */
  165. def withChildren(lst: Spatial*): Node = {
  166. for (c <- lst) n.attachChild(c)
  167. n
  168. }
  169. def +=(spatial: Spatial) = n.attachChild(spatial)
  170. def :+(spatial: Spatial) = {
  171. n += spatial
  172. n
  173. }
  174. def :+(light: Light) = {
  175. n.addLight(light)
  176. n
  177. }
  178. def -=(spatial: Spatial) = n.detachChild(spatial)
  179. def :-(spatial: Spatial) = {
  180. n -= spatial
  181. n
  182. }
  183. def :-(light: Light) = {
  184. n.removeLight(light)
  185. n
  186. }
  187. def depthFirst(cb: Spatial => Unit) =
  188. n.depthFirstTraversal(new SceneGraphVisitor() {
  189. override def visit(s: Spatial) = cb(s)
  190. })
  191. def observableDepthFirst(): Observable[Spatial] = {
  192. def loop(
  193. subscriber: Subscriber[Spatial],
  194. spatials: LazyList[Spatial]
  195. ): Task[Unit] =
  196. spatials match {
  197. // spatial can be either a node or a geometry, but it's not a sealed trait
  198. case head #:: tail =>
  199. head match {
  200. case g: Geometry =>
  201. Task.deferFuture(subscriber.onNext(g)).flatMap {
  202. case Continue =>
  203. loop(subscriber, tail)
  204. case Stop =>
  205. Task(subscriber.onComplete())
  206. }
  207. case node: Node =>
  208. val children = node.children
  209. Task.deferFuture(subscriber.onNext(node)).flatMap {
  210. case Continue =>
  211. loop(subscriber, children #::: tail)
  212. case Stop =>
  213. Task(subscriber.onComplete())
  214. }
  215. // case _ => loop(subscriber, tail)
  216. }
  217. case LazyList() => Task.unit
  218. }
  219. Observable.create(OverflowStrategy.Unbounded) { sub =>
  220. implicit val sched = sub.scheduler
  221. loop(sub, LazyList(n)).runToFuture
  222. }
  223. }
  224. def breadthFirst(cb: Spatial => Unit) =
  225. n.breadthFirstTraversal(new SceneGraphVisitor() {
  226. override def visit(s: Spatial) = cb(s)
  227. })
  228. def observableBreadthFirst(): Observable[Spatial] = {
  229. def loop(
  230. subscriber: Subscriber[Spatial],
  231. spatials: LazyList[Spatial]
  232. ): Task[Unit] =
  233. spatials match {
  234. // spatial can be either a node or a geometry, but it's not a sealed trait
  235. case head #:: tail =>
  236. head match {
  237. case g: Geometry =>
  238. Task.deferFuture(subscriber.onNext(g)).flatMap {
  239. case Continue =>
  240. loop(subscriber, tail)
  241. case Stop =>
  242. Task(subscriber.onComplete())
  243. }
  244. case node: Node =>
  245. val children = node.children
  246. Task.deferFuture(subscriber.onNext(node)).flatMap {
  247. case Continue =>
  248. loop(subscriber, tail #::: children)
  249. case Stop =>
  250. Task(subscriber.onComplete())
  251. }
  252. case unknown =>
  253. Task.deferFuture(subscriber.onNext(unknown)).flatMap {
  254. case Continue =>
  255. loop(subscriber, tail)
  256. case Stop =>
  257. Task(subscriber.onComplete())
  258. }
  259. }
  260. case LazyList() => Task.unit
  261. }
  262. Observable.create(OverflowStrategy.Unbounded) { sub =>
  263. implicit val sched = sub.scheduler
  264. loop(sub, LazyList(n)).runToFuture
  265. }
  266. }
  267. def withControl[C <: Control](ctrl: C) = {
  268. n.addControl(ctrl)
  269. n
  270. }
  271. def withLocalTranslation(dir: ImVector3f) = {
  272. n.setLocalTranslation(dir.mutable)
  273. n
  274. }
  275. def withRotate(xAngle: Float, yAngle: Float, zAngle: Float) = {
  276. n.rotate(xAngle, yAngle, zAngle)
  277. n
  278. }
  279. def localRotation = n.getLocalRotation()
  280. def localTranslation = n.getLocalTranslation()
  281. }
  282. implicit final class CameraNodeExt(private val cn: CameraNode) {
  283. def withControlDir(controlDir: ControlDirection) = {
  284. cn.setControlDir(controlDir)
  285. cn
  286. }
  287. def withLookAt(position: ImVector3f, upVector: ImVector3f) = {
  288. cn.lookAt(position.mutable, upVector.mutable)
  289. cn
  290. }
  291. }
  292. implicit final class GeometryExt(private val geom: Geometry) {
  293. def withMaterial(mat: Material) = {
  294. geom.setMaterial(mat)
  295. geom
  296. }
  297. }
  298. implicit final class EntityDataExt(private val ed: EntityData)
  299. extends AnyVal {
  300. def query = new EntityQuery(ed)
  301. // def entities[T <: EntityComponent](entities: Seq[T])
  302. }
  303. implicit final class EntityExt(private val e: EntityId) extends AnyVal {
  304. def withComponents(classes: EntityComponent*)(implicit
  305. ed: EntityData
  306. ): EntityId = {
  307. ed.setComponents(e, classes: _*)
  308. e
  309. }
  310. }
  311. implicit final class ActorRefExt[Req](private val a: ActorRef[Req])
  312. extends AnyVal {
  313. import akka.actor.typed.scaladsl.AskPattern._
  314. /**
  315. * Same as [[ask]] but returns a [[Task]]
  316. */
  317. def askL[Res](
  318. replyTo: ActorRef[Res] => Req
  319. )(implicit timeout: Timeout, scheduler: Scheduler): Task[Res] = {
  320. Task.deferFuture(a.ask(replyTo)(timeout, scheduler))
  321. }
  322. def ??[Res](
  323. replyTo: ActorRef[Res] => Req
  324. )(implicit timeout: Timeout, scheduler: Scheduler): Task[Res] =
  325. askL(replyTo)
  326. /**
  327. * Same as [[tell]], but wrapped in a Task
  328. *
  329. * @param msg
  330. * @return
  331. */
  332. def tellL(msg: Req) = UIO(a.tell(msg))
  333. /**
  334. * Same as [[tell]], but wrapped in a Task
  335. *
  336. * @param msg
  337. * @return
  338. */
  339. def !!(msg: Req) = tellL(msg)
  340. }
  341. implicit final class InputManagerExt(private val inputManager: InputManager)
  342. extends AnyVal {
  343. def withMapping(mapping: String, triggers: Trigger*): InputManager = {
  344. inputManager.addMapping(mapping, triggers: _*)
  345. inputManager
  346. }
  347. def withListener(listener: InputListener, mappings: String*) = {
  348. inputManager.addListener(listener, mappings: _*)
  349. inputManager
  350. }
  351. def withEnumMappings[T <: EnumEntry](
  352. mappingEnum: Enum[T]
  353. )(mappingFn: T => Seq[Trigger]): InputManager = {
  354. for (entry <- mappingEnum.values) {
  355. val mappings = mappingFn(entry)
  356. inputManager.addMapping(entry.entryName, mappings: _*)
  357. }
  358. inputManager
  359. }
  360. def observableAction(mappingNames: String*): Observable[ActionEvent] = {
  361. Observable.create(OverflowStrategy.DropOld(10)) { sub =>
  362. val c = SingleAssignCancelable()
  363. val al = new ActionListener {
  364. override def onAction(
  365. binding: String,
  366. value: Boolean,
  367. tpf: Float
  368. ): Unit = {
  369. if (
  370. sub.onNext(ActionEvent(Action(binding), value, tpf)) == Ack.Stop
  371. ) {
  372. sub.onComplete()
  373. c.cancel()
  374. }
  375. }
  376. }
  377. inputManager.addListener(al, mappingNames: _*)
  378. c := Cancelable(() => inputManager.removeListener(al))
  379. c
  380. }
  381. }
  382. def enumObservableAction[T <: EnumEntry](
  383. mappingEnum: Enum[T]
  384. ): Observable[EnumActionEvent[T]] = {
  385. Observable.create(OverflowStrategy.DropOld(10)) { sub =>
  386. val c = SingleAssignCancelable()
  387. val entryNames = mappingEnum.values.map(_.entryName)
  388. val al = new ActionListener {
  389. override def onAction(
  390. binding: String,
  391. value: Boolean,
  392. tpf: Float
  393. ): Unit = {
  394. mappingEnum.withNameOption(binding).foreach { b =>
  395. if (sub.onNext(EnumActionEvent(b, value, tpf)) == Ack.Stop) {
  396. sub.onComplete()
  397. c.cancel()
  398. }
  399. }
  400. }
  401. }
  402. inputManager.addListener(al, entryNames: _*)
  403. c := Cancelable(() => inputManager.removeListener(al))
  404. c
  405. }
  406. }
  407. def analogObservable(mappingNames: String*): Observable[AnalogEvent] = {
  408. Observable.create(OverflowStrategy.DropOld(50)) { sub =>
  409. val c = SingleAssignCancelable()
  410. val al = new AnalogListener {
  411. override def onAnalog(
  412. binding: String,
  413. value: Float,
  414. tpf: Float
  415. ): Unit = {
  416. if (
  417. sub.onNext(AnalogEvent(Action(binding), value, tpf)) == Ack.Stop
  418. ) {
  419. sub.onComplete()
  420. c.cancel()
  421. }
  422. }
  423. }
  424. inputManager.addListener(al, mappingNames: _*)
  425. c := Cancelable(() => inputManager.removeListener(al))
  426. c
  427. }
  428. }
  429. def enumAnalogObservable[T <: EnumEntry](
  430. mappingEnum: Enum[T]
  431. ): Observable[EnumAnalogEvent[T]] = {
  432. Observable.create(OverflowStrategy.DropOld(50)) { sub =>
  433. val c = SingleAssignCancelable()
  434. val entryNames = mappingEnum.values.map(_.entryName)
  435. val al = new AnalogListener {
  436. override def onAnalog(
  437. binding: String,
  438. value: Float,
  439. tpf: Float
  440. ): Unit = {
  441. mappingEnum.withNameOption(binding).foreach { b =>
  442. if (sub.onNext(EnumAnalogEvent(b, value, tpf)) == Ack.Stop) {
  443. sub.onComplete()
  444. c.cancel()
  445. }
  446. }
  447. }
  448. }
  449. inputManager.addListener(al, entryNames: _*)
  450. c := Cancelable(() => inputManager.removeListener(al))
  451. c
  452. }
  453. }
  454. }
  455. implicit final class PhysicsSpaceExt(private val space: PhysicsSpace)
  456. extends AnyVal {
  457. def collisionObservable(): Observable[PhysicsCollisionEvent] = {
  458. Observable.create(OverflowStrategy.DropOld(50)) { sub =>
  459. val c = SingleAssignCancelable()
  460. val cl = new PhysicsCollisionListener {
  461. override def collision(event: PhysicsCollisionEvent): Unit = {
  462. if (sub.onNext(event) == Ack.Stop) {
  463. sub.onComplete()
  464. c.cancel()
  465. }
  466. }
  467. }
  468. space.addCollisionListener(cl)
  469. c := Cancelable(() => space.removeCollisionListener(cl))
  470. c
  471. }
  472. }
  473. def prePhysicsTickObservable(): Observable[PrePhysicsTickEvent] = {
  474. Observable.create(OverflowStrategy.DropOld(50)) { sub =>
  475. val c = SingleAssignCancelable()
  476. val cl = new PhysicsTickListener {
  477. override def prePhysicsTick(space: PhysicsSpace, tpf: Float): Unit = {
  478. val event = new PrePhysicsTickEvent(space)
  479. if (sub.onNext(event) == Ack.Stop) {
  480. sub.onComplete()
  481. c.cancel()
  482. }
  483. }
  484. override def physicsTick(space: PhysicsSpace, tpf: Float): Unit = {}
  485. }
  486. space.addTickListener(cl)
  487. c := Cancelable(() => space.removeTickListener(cl))
  488. c
  489. }
  490. }
  491. def physicsTickObservable(): Observable[PhysicsTickEvent] = {
  492. Observable.create(OverflowStrategy.DropOld(50)) { sub =>
  493. val c = SingleAssignCancelable()
  494. val cl = new PhysicsTickListener {
  495. override def prePhysicsTick(
  496. space: PhysicsSpace,
  497. tpf: Float
  498. ): Unit = {}
  499. override def physicsTick(space: PhysicsSpace, tpf: Float): Unit = {
  500. val event = new PhysicsTickEvent(space)
  501. if (sub.onNext(event) == Ack.Stop) {
  502. sub.onComplete()
  503. c.cancel()
  504. }
  505. }
  506. }
  507. space.addTickListener(cl)
  508. c := Cancelable(() => space.removeTickListener(cl))
  509. c
  510. }
  511. }
  512. //TODO Consider creating a typeclass for this
  513. def +=(anyObject: Any) = space.add(anyObject)
  514. def :+(anyObject: Any) = {
  515. space.add(anyObject)
  516. space
  517. }
  518. def :-(anyObject: Any) = {
  519. space.remove(anyObject)
  520. space
  521. }
  522. def +=(spatial: Spatial) = space.addAll(spatial)
  523. def :+(spatial: Spatial) = {
  524. space.addAll(spatial)
  525. space
  526. }
  527. def :-(spatial: Spatial) = {
  528. space.removeAll(spatial)
  529. space
  530. }
  531. // def asRef = Ref[Task].of(space)
  532. }
  533. implicit final class Vector3fExt(private val v: Vector3f) extends AnyVal {
  534. //TODO add more operations
  535. def +=(that: Vector3f) = v.addLocal(that)
  536. def +=(f: Float) = v.addLocal(f, f, f)
  537. def +=(that: ImVector3f) = v.addLocal(that.x, that.y, that.z)
  538. def +=:(that: ImVector3f) = v += that
  539. def *=(that: Vector3f) = v.multLocal(that)
  540. def *=(that: ImVector3f) = v.multLocal(that.x, that.y, that.z)
  541. def *=:(that: ImVector3f) = v *= that
  542. def -=(that: Vector3f) = v.subtractLocal(that)
  543. def -=(that: ImVector3f) = v.subtractLocal(that.x, that.y, that.z)
  544. def -=:(that: ImVector3f) = v *= that
  545. def /=(that: Vector3f) = v.divideLocal(that)
  546. def /=(that: ImVector3f) = v.divideLocal(that.mutable)
  547. def /=:(that: ImVector3f) = v *= that
  548. def unary_- = v.negateLocal()
  549. def immutable = ImVector3f(v.x, v.y, v.z)
  550. }
  551. implicit final class ImVector3fExt(private val v: ImVector3f) extends AnyVal {
  552. def +(that: ImVector3f) = v.copy(v.x + that.x, v.y + that.y, v.z + that.z)
  553. def +(f: Float): ImVector3f = v.copy(v.x + f, v.y + f, v.z + f)
  554. def *(that: ImVector3f) = v.copy(v.x * that.x, v.y * that.y, v.z * that.z)
  555. def *(f: Float): ImVector3f =
  556. v.copy(v.x * f, v.y * f, v.z * f)
  557. // v * ImVector3f(f, f, f)
  558. def -(that: ImVector3f) = v.copy(v.x - that.x, v.y - that.y, v.z - that.z)
  559. def -(f: Float): ImVector3f = v.copy(v.x - f, v.y - f, v.z - f)
  560. def /(that: ImVector3f) = v.copy(v.x / that.x, v.y / that.y, v.z / that.z)
  561. def /(f: Float): ImVector3f = v.copy(v.x / f, v.y / f, v.z / f)
  562. def unary_- = v.copy(-v.x, -v.y, -v.z)
  563. def mutable = new Vector3f(v.x, v.y, v.z)
  564. }
  565. // val TasktoUIO = new FunctionK[Task, UIO] {
  566. // def apply[T](f: Task[T]): UIO[T] =
  567. // f.hideErrors
  568. // }
  569. implicit final class JavaFxUIExt(private val jfxui: JavaFxUI) extends AnyVal {
  570. def +=(node: scalafx.scene.Node) = jfxui.attachChild(node)
  571. def -=(node: scalafx.scene.Node) = jfxui.detachChild(node)
  572. }
  573. }