mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-09 00:09:55 +00:00
Transform gizmos (#10)
This PR adds functional transform gizmos, along with some new frontend UI to configure their state. ## Work Done - [x] Global / Local space toggling - [x] Gizmo type toggling - [x] Translation gizmo - [x] Axis-constrained translation - [x] Plane-constrained translation - [x] Screen-space translation - [x] Rotation gizmo - [x] Axis-constrained rotation - [x] Screen-space rotation - [x] "Trackball" rotation - [ ] Scale gizmo - [ ] Single-axis non-uniform scaling - [ ] Two-axis non-uniform scaling - [ ] Uniform scaling - [ ] Combination gizmos - [x] Translation + Rotation - [ ] Translation + Scale - [ ] Rotation + Scale - [ ] Translation + Rotation + Scale ## Future Enhancements ### Better visual feedback during transformations Most DCCs update the visual representation of the transform gizmo while it's in the process of being actively manipulated. (For example, Blender draws lines in world-space to indicate that the transformation is constrained to a particular axis or plane, hides widgets that aren't participating in the current transformation, and illustrates the angle of difference between the original and updated rotation.) At a minimum, we should indicate the current widget being manipulated with a color change (like we already do when the widget is being hovered). This requires a bit of additional state management effort but shouldn't be overly difficult to add in a future PR. ### Better visual representation for rotation widgets Currently, each axis of rotation is represented on the gizmo with a full-circle, unlit "ring" shape. This can make it difficult to tell which half of the ring is closer to the viewer, and thus to get an accurate understanding of how the transform is currently oriented, without moving the camera around. A [previous version of this work](#4) bisected the rings along a view-oriented plane and rendered the "back-side" ring halves with reduced opacity. This PR uses static meshes instead of an immediate-mode drawing API to render the gizmos, so a custom shader would be the ideal way to reproduce that effect. That effort should be deferred until after upgrading to Bevy 0.12, as it brings some changes which would likely require some updates to that code. ### Better visual representation for translation arrows A [previous version of this work](#4) faded the opacity of the translation arrows when they were very close to parallel with the view direction — an idea that was borrowed from Blender. It's a nice UX enhancement because translating along an axis that's very close to parallel with the view direction can be janky and error-prone, so it makes sense to disallow it and instead provide an unobstructed view of the screen-space translation widget. This would also require a custom shader, in addition to some extra state management logic (to disable the widgets when their opacity is zeroed out). Given that it's only a "nice-to-have," I think it makes sense to backlog this for the time being. ## Known Issues ### Picking There's currently a bug in `bevy_mod_picking` that affects the hit-testing of the gizmo widgets. The gizmos are currently _rendering_ on top of everything else (via a second camera and render-layer configurations), but the picking behavior doesn't take the rendering order into account. This means that portions of the gizmo widgets which are occluded by geometry in the world cannot be interacted with (because the occluding geometry interrupts the picking raycast). > Update: This is fixed in the new release of `bevy_mod_picking` for Bevy 0.12. ### Delay on first-render The first time a new widget shape is spawned, there's a noticeable delay (of 1 to several seconds) before the widget is rendered, during which the UI becomes unresponsive. This is likely the same issue that triggers a much longer delay when the glTF scene is being spawned. I'd like to see if Bevy 0.12 resolves or mitigates this issue before addressing it on our end, but if it doesn't, I think we can simply spawn an instance of each of the gizmo widgets off-screen on application startup to prevent lag spikes from happening while a user is interacting with the scene. ### Non-deterministic hover states The widget colors are currently being updated by, e.g., multiplying on mouser-over and dividing on mouse-out. Occasionally those events happen in quick enough succession that our mouse-out listener is not invoked, so a widget gets "stuck" in its hovered state. The fix for this is trivial but fairly annoying, so I haven't gotten around to it yet. 🙂 > Update: This issue mostly occurs when in world-space mode. The gizmo is torn > down and re-built from scratch once a drag event completes, which can despawn > the gizmo while an element is still hovered. The solution will likely involve > completely separate materials for default and hover states, similar to how > `bevy_mod_picking`'s highlight plugin works. ### Object Selection There's still some jank happening with object selection and camera orbiting, as mentioned in #8. I don't anticipate this being overly difficult to resolve, but I'm deferring that work until more of the core functionality is built out since it's not a blocker for now. ### Skinned Mesh Selection There's also still the issue of the skinned mesh being unselectable — which, it turns out, is [technically not actually the case](https://github.com/bevyengine/bevy/issues/4971). Rather, the picking raycast "sees" the mesh as if it were still in its original rest pose, because the mesh deformations are processed on the GPU and not reflected back to the CPU-side representation. I think the ideal workaround for this will be to attach capsule-shaped Rapier colliders to the skeleton (similar to Unreal Engine's auto-generated "physics assets" for skeletal meshes) and use those for picking instead. This would automatically enable skeletal pose manipulation (by transforming the individual joints), which is already a long term goal. It also makes more sense from a practical standpoint, as the mesh itself cannot be independently transformed while it's bound to the skeleton, so there's little point in directly selecting it.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
@fortawesome:registry=https://npm.fontawesome.com/
|
||||
//npm.fontawesome.com/:_authToken=104779DB-FD41-4BEA-BDB4-B1845D758A79
|
||||
Generated
+257
-1
@@ -798,10 +798,12 @@ dependencies = [
|
||||
"bevy_picking_core",
|
||||
"bevy_picking_highlight",
|
||||
"bevy_picking_input",
|
||||
"bevy_picking_rapier",
|
||||
"bevy_picking_raycast",
|
||||
"bevy_picking_selection",
|
||||
"bevy_picking_sprite",
|
||||
"bevy_picking_ui",
|
||||
"bevy_rapier3d",
|
||||
"bevy_reflect",
|
||||
"bevy_render",
|
||||
"bevy_text",
|
||||
@@ -895,6 +897,24 @@ dependencies = [
|
||||
"bevy_window",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_picking_rapier"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8214cf54dcf14ee498e94411d51c7ddc3bb48a8a2e88179fd81b073e67316e6a"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_ecs",
|
||||
"bevy_math",
|
||||
"bevy_picking_core",
|
||||
"bevy_rapier3d",
|
||||
"bevy_reflect",
|
||||
"bevy_render",
|
||||
"bevy_transform",
|
||||
"bevy_utils",
|
||||
"bevy_window",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_picking_raycast"
|
||||
version = "0.16.0"
|
||||
@@ -965,6 +985,19 @@ version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72c7586401a46f7d8e436028225c1df5288f2e0082d066b247a82466fea155c6"
|
||||
|
||||
[[package]]
|
||||
name = "bevy_rapier3d"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12402872b857ba490f1040ab6212915bd9bf25f8584b31f2c43cef41b33f3be4"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"bitflags 1.3.2",
|
||||
"log",
|
||||
"nalgebra",
|
||||
"rapier3d",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_reflect"
|
||||
version = "0.11.3"
|
||||
@@ -1621,6 +1654,20 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.8"
|
||||
@@ -1631,6 +1678,40 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"memoffset 0.9.0",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.16"
|
||||
@@ -1695,6 +1776,12 @@ dependencies = [
|
||||
"nohash-hasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||
|
||||
[[package]]
|
||||
name = "emath"
|
||||
version = "0.23.0"
|
||||
@@ -2438,6 +2525,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
@@ -2491,6 +2584,16 @@ dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7574c1cf36da4798ab73da5b215bbf444f50718207754cb522201d78d1cd0ff2"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.3"
|
||||
@@ -2506,6 +2609,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metal"
|
||||
version = "0.24.0"
|
||||
@@ -2589,6 +2701,34 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "307ed9b18cc2423f29e83f84fd23a8e73628727990181f18641a8b5dc2ab1caa"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"glam",
|
||||
"matrixmultiply",
|
||||
"nalgebra-macros",
|
||||
"num-complex",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"simba",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nalgebra-macros"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.7.0"
|
||||
@@ -2627,7 +2767,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.6.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2695,6 +2835,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.3"
|
||||
@@ -2734,6 +2883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2943,6 +3093,27 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parry3d"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b55dc0e6db79bddbc5fd583569f7356cdcc63e1e9b2b93a9ab70dd8e717160e0"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"downcast-rs",
|
||||
"either",
|
||||
"nalgebra",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
"simba",
|
||||
"slab",
|
||||
"smallvec",
|
||||
"spade",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
@@ -3057,12 +3228,39 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab"
|
||||
|
||||
[[package]]
|
||||
name = "rapier3d"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62a8a0bd9d3135f7b4eb45d0796540e7bab47b6b7c974f90567ccc5a0454f42b"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"arrayvec",
|
||||
"bit-vec",
|
||||
"bitflags 1.3.2",
|
||||
"crossbeam",
|
||||
"downcast-rs",
|
||||
"instant",
|
||||
"nalgebra",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"parry3d",
|
||||
"rustc-hash",
|
||||
"simba",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "rectangle-pack"
|
||||
version = "0.4.2"
|
||||
@@ -3128,6 +3326,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b"
|
||||
|
||||
[[package]]
|
||||
name = "robust"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30"
|
||||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.17.1"
|
||||
@@ -3179,6 +3383,15 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "safe_arch"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -3240,6 +3453,19 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
|
||||
|
||||
[[package]]
|
||||
name = "simba"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-complex",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"wide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
@@ -3282,6 +3508,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spade"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab085cfe5486093ca290034bd2d424e283f9725b4bcf8ba5a7f6c7612b59841c"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"robust",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.2.0+1.5.4"
|
||||
@@ -3312,6 +3549,9 @@ dependencies = [
|
||||
"bevy-inspector-egui",
|
||||
"bevy_mod_outline",
|
||||
"bevy_mod_picking",
|
||||
"bevy_mod_raycast",
|
||||
"bevy_rapier3d",
|
||||
"bitflags 2.4.0",
|
||||
"console_error_panic_hook",
|
||||
"parking_lot",
|
||||
"wasm-bindgen",
|
||||
@@ -3562,6 +3802,12 @@ dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
@@ -3866,6 +4112,16 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "0.7.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c68938b57b33da363195412cfc5fc37c9ed49aa9cfe2156fde64b8d2c9498242"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"safe_arch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.0.2"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./lib/decorators/on";
|
||||
export { default as spread } from "./lib/directives/spread";
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ElementPart, nothing } from "lit";
|
||||
import { directive, Directive } from "lit/directive.js";
|
||||
|
||||
interface Stringable {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple implementation of React's spread operator (`...`) for Lit. Assigns
|
||||
* each `{ [key]: value }` entry in the provided object as an HTML attribute to
|
||||
* the element it's invoked on.
|
||||
*
|
||||
* React:
|
||||
* @example
|
||||
* ```jsx
|
||||
* export default (props) => <div {...props} />
|
||||
* ```
|
||||
*
|
||||
* Lit:
|
||||
* @example
|
||||
* ```typescript
|
||||
* export class FooElement extends LitElement {
|
||||
* props: Record<string, string> = {};
|
||||
* override render = () => html`<div ${spread(props)}></div>`
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @note
|
||||
* The object entries are assigned as HTML _attributes_, not _properties_, so
|
||||
* each enumerable value in the provided object must have a meaningnful
|
||||
* `toString()` implementation.
|
||||
*/
|
||||
export default directive(class Spread extends Directive {
|
||||
override render(_: Record<string, Stringable>) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
override update(part: ElementPart, [attrs]: [Record<string, Stringable>]) {
|
||||
for (let [key, value] of Object.entries(attrs)) {
|
||||
part.element.setAttribute(key, value.toString());
|
||||
}
|
||||
return part;
|
||||
}
|
||||
});
|
||||
Generated
+577
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-pro": "^6.4.2",
|
||||
"lit": "^2.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.3.0"
|
||||
@@ -2387,6 +2388,14 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-pro": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-pro/-/6.4.2/fontawesome-pro-6.4.2.tgz",
|
||||
"integrity": "sha512-b7VtzrgoASuCbnWJ1A17Sw3wemaFGukyBk0Y2ks/rgcnhlJ1uzdyv2uM3QbEaq9OgyZAKGJD1Tli6qHRI68zSQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -2536,6 +2545,17 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
|
||||
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
@@ -3671,6 +3691,28 @@
|
||||
"@types/chai": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.44.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz",
|
||||
"integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint-scope": {
|
||||
"version": "3.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz",
|
||||
"integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.2.tgz",
|
||||
@@ -3683,6 +3725,13 @@
|
||||
"integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz",
|
||||
"integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.14.199",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz",
|
||||
@@ -3831,6 +3880,181 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
|
||||
"integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/floating-point-hex-parser": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
|
||||
"integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
|
||||
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
|
||||
"integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
|
||||
"integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
|
||||
"@webassemblyjs/helper-api-error": "1.11.6",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
|
||||
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
|
||||
"integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-buffer": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/wasm-gen": "1.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
|
||||
"integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/leb128": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
|
||||
"integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/utf8": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
|
||||
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
|
||||
"integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-buffer": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-section": "1.11.6",
|
||||
"@webassemblyjs/wasm-gen": "1.11.6",
|
||||
"@webassemblyjs/wasm-opt": "1.11.6",
|
||||
"@webassemblyjs/wasm-parser": "1.11.6",
|
||||
"@webassemblyjs/wast-printer": "1.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
|
||||
"integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/ieee754": "1.11.6",
|
||||
"@webassemblyjs/leb128": "1.11.6",
|
||||
"@webassemblyjs/utf8": "1.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
|
||||
"integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-buffer": "1.11.6",
|
||||
"@webassemblyjs/wasm-gen": "1.11.6",
|
||||
"@webassemblyjs/wasm-parser": "1.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
|
||||
"integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-api-error": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/ieee754": "1.11.6",
|
||||
"@webassemblyjs/leb128": "1.11.6",
|
||||
"@webassemblyjs/utf8": "1.11.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
|
||||
"integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
|
||||
@@ -3902,6 +4126,16 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-import-assertions": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
|
||||
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"acorn": "^8"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
|
||||
@@ -3948,6 +4182,16 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@@ -4504,6 +4748,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
|
||||
"integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -4606,6 +4860,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -4889,6 +5150,20 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
|
||||
"integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
|
||||
@@ -4922,6 +5197,13 @@
|
||||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
|
||||
"integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
|
||||
@@ -4977,6 +5259,30 @@
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope/node_modules/estraverse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
@@ -5002,6 +5308,19 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/esrecurse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
@@ -5032,6 +5351,16 @@
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -5377,6 +5706,13 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@@ -5950,6 +6286,37 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jju": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz",
|
||||
@@ -6113,6 +6480,16 @@
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-utils": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
@@ -6260,6 +6637,13 @@
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -6442,6 +6826,13 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz",
|
||||
@@ -7109,6 +7500,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
@@ -7432,6 +7833,25 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/secure-compare": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
|
||||
@@ -7471,6 +7891,16 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
|
||||
"integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -7743,6 +8173,16 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||
@@ -7794,6 +8234,71 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.24.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz",
|
||||
"integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.9",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
|
||||
"integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^3.1.1",
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"terser": "^5.16.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||
@@ -8492,6 +8997,20 @@
|
||||
"wasm-pack": "run.js"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wcwidth": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||
@@ -8510,6 +9029,64 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.89.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
|
||||
"integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^1.0.0",
|
||||
"@webassemblyjs/ast": "^1.11.5",
|
||||
"@webassemblyjs/wasm-edit": "^1.11.5",
|
||||
"@webassemblyjs/wasm-parser": "^1.11.5",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-import-assertions": "^1.9.0",
|
||||
"browserslist": "^4.14.5",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.15.0",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.2.0",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.7",
|
||||
"watchpack": "^2.4.0",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"webpack-cli": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"wasm-pack": "^0.12.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-pro": "^6.4.2",
|
||||
"lit": "^2.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.3.0"
|
||||
|
||||
@@ -10,6 +10,87 @@
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
padding: 10px 12px;
|
||||
pointer-events: auto;
|
||||
|
||||
// TODO: Copypasta
|
||||
background: #0006;
|
||||
color: #FFFC;
|
||||
backdrop-filter: blur(16px);
|
||||
transition: {
|
||||
property: background, color;
|
||||
duration: 0.1s;
|
||||
timing-function: linear;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border: {
|
||||
top-left-radius: 8px;
|
||||
bottom-left-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border: {
|
||||
top-right-radius: 8px;
|
||||
bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #0008;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
&:has(:checked) {
|
||||
background: #7A2C34E3;
|
||||
color: #FFF;
|
||||
|
||||
&:hover {
|
||||
background: #90363F;
|
||||
}
|
||||
}
|
||||
|
||||
&:has(:disabled) {
|
||||
background: #0003 !important;
|
||||
color: #FFF6 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.help {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { on } from "@storyteller/framework";
|
||||
import * as studio from "@storyteller/studio";
|
||||
import { ViewportSize } from "@storyteller/studio";
|
||||
import { TransformSpace, TransformType, ViewportSize } from "@storyteller/studio";
|
||||
import { match } from "@storyteller/utility";
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
|
||||
import "./button.element";
|
||||
import "./help.element";
|
||||
import "./icons/fa-circle-info.element";
|
||||
import "./icon.element";
|
||||
|
||||
import styles from "./app.element.scss?inline";
|
||||
|
||||
@@ -16,6 +17,9 @@ export class AppElement extends LitElement {
|
||||
|
||||
@state() showHelp = false;
|
||||
|
||||
@state() transformSpace = TransformSpace.Local;
|
||||
@state() transformType = TransformType.Translate;
|
||||
|
||||
#studioCanvas?: HTMLCanvasElement | null;
|
||||
|
||||
override connectedCallback(): void {
|
||||
@@ -30,10 +34,71 @@ export class AppElement extends LitElement {
|
||||
studio.resize(new ViewportSize(width, height));
|
||||
}
|
||||
|
||||
@on("keydown")
|
||||
@on("keyup")
|
||||
pipeKeyboardEvents(event: KeyboardEvent): void {
|
||||
// When the user interacts with one of the web UI elements, the Bevy
|
||||
// canvas will lose focus, preventing it from receiving keyboard events.
|
||||
// As a workaround, we can listen for any events that have bubbled up to
|
||||
// the app root and repeat them for the canvas.
|
||||
//
|
||||
// This does unfortunately burden the web front-end with some additional
|
||||
// responsibility: when we handle a keyboard event and _don't_ want the
|
||||
// Bevy app to receive it, we'll need to call `event.stopPropagation()` in
|
||||
// the handler to prevent it from reaching this listener.
|
||||
const {
|
||||
type,
|
||||
code, charCode, key, keyCode, location, repeat,
|
||||
altKey, ctrlKey, metaKey, shiftKey,
|
||||
} = event;
|
||||
|
||||
this.#studioCanvas?.dispatchEvent(new KeyboardEvent(type, {
|
||||
code, charCode, key, keyCode, location, repeat,
|
||||
altKey, ctrlKey, metaKey, shiftKey,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
}));
|
||||
}
|
||||
|
||||
toggleHelp(): void {
|
||||
this.showHelp = !this.showHelp;
|
||||
}
|
||||
|
||||
onTransformSpaceChange(event: InputEvent): void {
|
||||
const { value } = event.target as HTMLInputElement;
|
||||
const space = match (value, {
|
||||
"local": () => TransformSpace.Local,
|
||||
"world": () => TransformSpace.World,
|
||||
_: () => {
|
||||
throw new Error(`Unknown transform space: "${value}"`);
|
||||
}
|
||||
});
|
||||
|
||||
this.transformSpace = space;
|
||||
studio.setTransformSpace(space);
|
||||
}
|
||||
|
||||
onTransformTypeChange(event: InputEvent): void {
|
||||
const { name, checked } = event.target as HTMLInputElement;
|
||||
const flag = match (name, {
|
||||
"translate": () => TransformType.Translate,
|
||||
"rotate": () => TransformType.Rotate,
|
||||
"scale": () => TransformType.Scale,
|
||||
_: () => {
|
||||
throw new Error(`Unknown transform type: "${name}"`);
|
||||
}
|
||||
});
|
||||
|
||||
if (checked) {
|
||||
this.transformType |= flag;
|
||||
} else {
|
||||
this.transformType &= ~flag;
|
||||
}
|
||||
|
||||
studio.setTransformType(this.transformType);
|
||||
}
|
||||
|
||||
#initStudio(): HTMLCanvasElement | null {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
@@ -48,6 +113,58 @@ export class AppElement extends LitElement {
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
<!-- TODO: What's a "component"? -->
|
||||
<div class="toolbar" role="toolbar">
|
||||
<div class="toolbar-group" role="group">
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="cube">Local</sts-icon>
|
||||
<input type="radio"
|
||||
name="transform-space"
|
||||
value="local"
|
||||
?checked=${this.transformSpace === TransformSpace.Local}
|
||||
@input=${this.onTransformSpaceChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="globe">Global</sts-icon>
|
||||
<input type="radio"
|
||||
name="transform-space"
|
||||
value="world"
|
||||
?checked=${this.transformSpace === TransformSpace.World}
|
||||
@input=${this.onTransformSpaceChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toolbar-group" role="group">
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="up-down-left-right">Translate</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="translate"
|
||||
?checked=${this.transformType & TransformType.Translate}
|
||||
@input=${this.onTransformTypeChange}
|
||||
/>
|
||||
</label>
|
||||
<!-- disabled -->
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="rotate">Rotate</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="rotate"
|
||||
?checked=${this.transformType & TransformType.Rotate}
|
||||
@input=${this.onTransformTypeChange}
|
||||
/>
|
||||
</label>
|
||||
<label class="btn-toggle">
|
||||
<sts-icon icon="scale">Scale</sts-icon>
|
||||
<input type="checkbox"
|
||||
name="scale"
|
||||
disabled
|
||||
?checked=${this.transformType & TransformType.Scale}
|
||||
@input=${this.onTransformTypeChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sts-button class="btn-help"
|
||||
icon="circle-info"
|
||||
@click=${this.toggleHelp}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { match } from "@storyteller/utility";
|
||||
import { on } from "@storyteller/framework";
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { LitElement, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import styles from "./button.element.scss?inline";
|
||||
|
||||
import "./icon.element";
|
||||
|
||||
@customElement("sts-button")
|
||||
export class ButtonElement extends LitElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
@@ -15,16 +16,17 @@ export class ButtonElement extends LitElement {
|
||||
override tabIndex = 0;
|
||||
|
||||
@on("keydown")
|
||||
onKeyDown({ key }: KeyboardEvent) {
|
||||
if (/^( |Enter)$/.test(key))
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
if (/^( |Enter)$/.test(event.key)) {
|
||||
event.stopPropagation();
|
||||
this.click();
|
||||
}
|
||||
}
|
||||
|
||||
protected override render = () => html`
|
||||
${match (this.icon, {
|
||||
"circle-info": () => html`<fa-circle-info class="icon"></fa-circle-info>`,
|
||||
_: () => null,
|
||||
})}
|
||||
${this.icon
|
||||
? html`<sts-icon icon=${this.icon} aria-hidden="true"></sts-icon>`
|
||||
: nothing}
|
||||
<slot></slot>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
:host {
|
||||
display: flex;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.a11y-label {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.svg-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { spread } from "@storyteller/framework";
|
||||
import { match } from "@storyteller/utility";
|
||||
import { LitElement, TemplateResult, html, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { unsafeSVG } from "lit/directives/unsafe-svg.js";
|
||||
|
||||
import fasCircleInfo from "@fortawesome/fontawesome-pro/svgs/regular/circle-info.svg?raw";
|
||||
import fasCube from "@fortawesome/fontawesome-pro/svgs/regular/cube.svg?raw";
|
||||
import fasGlobe from "@fortawesome/fontawesome-pro/svgs/regular/globe.svg?raw";
|
||||
import fasUpDownLeftRight from "@fortawesome/fontawesome-pro/svgs/regular/arrows-up-down-left-right.svg?raw";
|
||||
import fasRotate from "@fortawesome/fontawesome-pro/svgs/regular/arrows-rotate.svg?raw";
|
||||
import fasScale from "@fortawesome/fontawesome-pro/svgs/regular/arrow-up-right-and-arrow-down-left-from-center.svg?raw";
|
||||
|
||||
import styles from "./icon.element.scss?inline";
|
||||
|
||||
@customElement("sts-icon")
|
||||
export class IconElement extends LitElement {
|
||||
static override styles = unsafeCSS(styles);
|
||||
|
||||
@property() icon = "";
|
||||
|
||||
protected override render = () => html`
|
||||
<span class="a11y-label">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<div class="svg-wrapper" role="presentation">
|
||||
${renderSvg(this.icon)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const RENDER_RESULTS = new Map<string, TemplateResult>();
|
||||
|
||||
function renderSvg(iconId: string) {
|
||||
if (RENDER_RESULTS.has(iconId))
|
||||
return RENDER_RESULTS.get(iconId)!;
|
||||
|
||||
const svgSrc = match (iconId, {
|
||||
"circle-info": () => fasCircleInfo,
|
||||
"cube": () => fasCube,
|
||||
"globe": () => fasGlobe,
|
||||
"up-down-left-right": () => fasUpDownLeftRight,
|
||||
"rotate": () => fasRotate,
|
||||
"scale": () => fasScale,
|
||||
_: () => null,
|
||||
});
|
||||
|
||||
if (!svgSrc) return nothing;
|
||||
|
||||
const [, attrsString, body] = svgSrc.match(/<svg(.+?)>(.+?)<\/svg>/)!;
|
||||
const attrs = Object.fromEntries(
|
||||
Array.from(attrsString.matchAll(/([-:_a-zA-Z0-9]+)(?:=['"]?([^'"]*)['"]?)/g))
|
||||
.map(([, attr, value]) => [attr, value])
|
||||
);
|
||||
|
||||
const result = html`
|
||||
<svg ${spread(attrs)}>
|
||||
${unsafeSVG(body)}
|
||||
</svg>
|
||||
`;
|
||||
|
||||
RENDER_RESULTS.set(iconId, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("fa-circle-info")
|
||||
export class CircleInfoIcon extends LitElement {
|
||||
override render = () => html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
height="1em"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<!--!
|
||||
Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com
|
||||
License - https://fontawesome.com/license (Commercial License)
|
||||
Copyright 2023 Fonticons, Inc.
|
||||
-->
|
||||
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"
|
||||
/>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.svg?raw" {
|
||||
const svg: string;
|
||||
export default svg;
|
||||
}
|
||||
+12
-5
@@ -13,14 +13,20 @@ name = "studio-standalone"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
bevy-inspector-egui = { version = "0.20.0" }
|
||||
# bevy_mod_outline = { version = "0.5.0" }
|
||||
bevy-inspector-egui = "0.20.0"
|
||||
bevy_mod_raycast = "0.15.0"
|
||||
bevy_rapier3d = "0.22.0"
|
||||
# bevy_mod_outline = "0.5.0"
|
||||
bitflags = "2.4.0"
|
||||
console_error_panic_hook = { version = "0.1.7", optional = true }
|
||||
parking_lot = "0.12.1"
|
||||
wasm-bindgen = { version = "0.2.84" }
|
||||
wasm-bindgen = "0.2.84"
|
||||
|
||||
[features]
|
||||
wasm = ["dep:console_error_panic_hook"]
|
||||
wasm = [
|
||||
"dep:console_error_panic_hook",
|
||||
"bevy_rapier3d/wasm-bindgen",
|
||||
]
|
||||
|
||||
[dependencies.bevy]
|
||||
workspace = true
|
||||
@@ -32,8 +38,9 @@ path = "../bevy_mod_outline"
|
||||
version = "0.16.0"
|
||||
default-features = false
|
||||
features = [
|
||||
"backend_raycast",
|
||||
"backend_bevy_ui",
|
||||
"backend_rapier",
|
||||
"backend_raycast",
|
||||
"backend_sprite",
|
||||
"debug",
|
||||
"selection",
|
||||
|
||||
Binary file not shown.
+14228
-14150
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use bevy_inspector_egui::quick::WorldInspectorPlugin;
|
||||
use bevy_mod_picking::debug::DebugPickingMode;
|
||||
@@ -81,3 +83,21 @@ fn draw_world_axes(mut gizmos: Gizmos, q: Query<&CameraController>) {
|
||||
draw_axis(Vec3::Y, Color::GREEN);
|
||||
draw_axis(Vec3::Z, Color::BLUE);
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(dead_code)]
|
||||
pub fn write_mat3(w: &mut impl fmt::Write, m: &Mat3) -> fmt::Result {
|
||||
writeln!(w, " {:+.3} {:+.3} {:+.3}", m.x_axis.x, m.x_axis.y, m.x_axis.z)?;
|
||||
writeln!(w, " {:+.3} {:+.3} {:+.3}", m.y_axis.x, m.y_axis.y, m.y_axis.z)?;
|
||||
writeln!(w, " {:+.3} {:+.3} {:+.3}", m.z_axis.x, m.z_axis.y, m.z_axis.z)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn print_mat3(m: &Mat3, label: impl fmt::Display) -> fmt::Result {
|
||||
let mut s = String::new();
|
||||
write_mat3(&mut s, m)?;
|
||||
info!("\n{label}\n{}", s.replace('+', " "));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use bevy::prelude::Color as BevyColor;
|
||||
|
||||
pub struct Color;
|
||||
|
||||
impl Color {
|
||||
// original: rgb(220, 53, 69)
|
||||
// brighter: rgb(255, 48, 68)
|
||||
pub const RED: BevyColor = BevyColor::rgb(1.000000, 0.188235, 0.266666);
|
||||
// original: rgb( 25, 135, 84)
|
||||
// brighter: rgb( 0, 187, 100)
|
||||
pub const GREEN: BevyColor = BevyColor::rgb(0.000000, 0.733333, 0.392157);
|
||||
// original: rgb( 13, 110, 253)
|
||||
// brighter: rgb( 47, 131, 255)
|
||||
pub const BLUE: BevyColor = BevyColor::rgb(0.184314, 0.513724, 1.000000);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use bevy::{
|
||||
core_pipeline::{clear_color::ClearColorConfig, fxaa::Fxaa},
|
||||
prelude::*,
|
||||
render::view::RenderLayers,
|
||||
};
|
||||
|
||||
use crate::MainCamera;
|
||||
|
||||
use super::GIZMO_RENDER_LAYER;
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) struct GizmoCamera;
|
||||
|
||||
pub(super) fn spawn_gizmo_camera(
|
||||
mut cmd: Commands,
|
||||
q_main_camera: Query<Entity, With<MainCamera>>,
|
||||
) {
|
||||
let ent = q_main_camera.single();
|
||||
|
||||
cmd.entity(ent).with_children(|cmd| {
|
||||
cmd.spawn((
|
||||
Name::new("Gizmo Camera"),
|
||||
GizmoCamera,
|
||||
Camera3dBundle {
|
||||
camera_3d: Camera3d {
|
||||
clear_color: ClearColorConfig::None,
|
||||
..default()
|
||||
},
|
||||
camera: Camera {
|
||||
hdr: true,
|
||||
order: 1,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
Fxaa::default(),
|
||||
));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use bevy::{math::vec3, pbr::NotShadowCaster, prelude::*, render::view::RenderLayers};
|
||||
use bevy_mod_picking::prelude::*;
|
||||
|
||||
use crate::interaction::TransformSpace;
|
||||
|
||||
use super::{
|
||||
on_gizmo_element_hover, on_gizmo_element_unhover,
|
||||
rotation::{on_rotate, on_rotation_end, on_rotation_start, RotationWidget},
|
||||
translation::{on_translate, on_translation_end, on_translation_start, TranslationWidget},
|
||||
GizmoMats, GizmoMeshes, TransformGizmoBundle, TransformGizmoRoot, GIZMO_RENDER_LAYER,
|
||||
};
|
||||
|
||||
#[inline]
|
||||
pub(super) fn spawn_combo_translation_rotation_gizmo(
|
||||
cmd: &mut Commands,
|
||||
ent: Entity,
|
||||
xform: &GlobalTransform,
|
||||
space: TransformSpace,
|
||||
normalizing_scale: f32,
|
||||
meshes: &GizmoMeshes,
|
||||
mats: &GizmoMats,
|
||||
) {
|
||||
cmd.entity(ent)
|
||||
.insert(TransformGizmoRoot)
|
||||
.with_children(|cmd| {
|
||||
cmd.spawn(TransformGizmoBundle::new(xform, space, normalizing_scale))
|
||||
.with_children(|cmd| {
|
||||
use super::{ROT_X, ROT_Y, ROT_Z};
|
||||
|
||||
// Center sphere
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::ScreenSpace,
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_ss.clone(),
|
||||
material: mats.translate_ss.clone(),
|
||||
transform: Transform::IDENTITY,
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_translation_start),
|
||||
On::<Pointer<Drag>>::run(on_translate),
|
||||
On::<Pointer<DragEnd>>::target_commands_mut(on_translation_end),
|
||||
));
|
||||
|
||||
// Rotate X
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Axis(Vec3::X),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ring.clone(),
|
||||
material: mats.rotate_x.clone(),
|
||||
transform: Transform::from_rotation(ROT_X),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_rotation_start),
|
||||
On::<Pointer<Drag>>::run(on_rotate),
|
||||
On::<Pointer<DragEnd>>::run(on_rotation_end),
|
||||
));
|
||||
// Translate X
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::X),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_arrow.clone(),
|
||||
material: mats.translate_x.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_X,
|
||||
translation: vec3(1.2, 0., 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_translation_start),
|
||||
On::<Pointer<Drag>>::run(on_translate),
|
||||
On::<Pointer<DragEnd>>::target_commands_mut(on_translation_end),
|
||||
));
|
||||
|
||||
// Rotate Y
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Axis(Vec3::Y),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ring.clone(),
|
||||
material: mats.rotate_y.clone(),
|
||||
transform: Transform::from_rotation(ROT_Y),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_rotation_start),
|
||||
On::<Pointer<Drag>>::run(on_rotate),
|
||||
On::<Pointer<DragEnd>>::run(on_rotation_end),
|
||||
));
|
||||
// Translate Y
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::Y),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_arrow.clone(),
|
||||
material: mats.translate_y.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Y,
|
||||
translation: vec3(0., 1.2, 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_translation_start),
|
||||
On::<Pointer<Drag>>::run(on_translate),
|
||||
On::<Pointer<DragEnd>>::target_commands_mut(on_translation_end),
|
||||
));
|
||||
|
||||
// Rotate Z
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Axis(Vec3::Z),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ring.clone(),
|
||||
material: mats.rotate_z.clone(),
|
||||
transform: Transform::from_rotation(ROT_Z),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_rotation_start),
|
||||
On::<Pointer<Drag>>::run(on_rotate),
|
||||
On::<Pointer<DragEnd>>::run(on_rotation_end),
|
||||
));
|
||||
// Translate Z
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::Z),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_arrow.clone(),
|
||||
material: mats.translate_z.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Z,
|
||||
translation: vec3(0., 0., 1.2),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_translation_start),
|
||||
On::<Pointer<Drag>>::run(on_translate),
|
||||
On::<Pointer<DragEnd>>::target_commands_mut(on_translation_end),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use bevy::{math::Affine3A, prelude::*};
|
||||
use bevy_mod_picking::prelude::*;
|
||||
|
||||
use crate::{
|
||||
interaction::TransformSpace,
|
||||
math::{IsNonUniform, MatrixExtras, VecExtras},
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) struct TransformGizmoRoot;
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) struct TransformGizmo;
|
||||
|
||||
#[derive(Event)]
|
||||
pub(super) struct RebuildGizmo;
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub(super) struct TransformGizmoBundle {
|
||||
name: Name,
|
||||
marker: TransformGizmo,
|
||||
spatial: SpatialBundle,
|
||||
scale: NaturalScale,
|
||||
}
|
||||
|
||||
#[derive(Component, Deref, Reflect)]
|
||||
pub(super) struct NaturalScale(pub Vec3);
|
||||
|
||||
#[derive(Component, Deref)]
|
||||
pub(super) struct InitialTransform(pub GlobalTransform);
|
||||
|
||||
impl TransformGizmoBundle {
|
||||
pub fn new(
|
||||
parent_xform: &GlobalTransform,
|
||||
space: TransformSpace,
|
||||
normalizing_scale: f32,
|
||||
) -> Self {
|
||||
let name = Name::new("TransformGizmo");
|
||||
let marker = TransformGizmo;
|
||||
|
||||
let Affine3A {
|
||||
matrix3,
|
||||
translation,
|
||||
} = parent_xform.affine();
|
||||
|
||||
let (scale, rotation) = matrix3.decompose_scale_rotation();
|
||||
let inv_scale = scale.inverse();
|
||||
let nat_scale = NaturalScale(Vec3::from_scale_mat(inv_scale));
|
||||
|
||||
match space {
|
||||
TransformSpace::Local => {
|
||||
let global_transform = GlobalTransform::from(Affine3A {
|
||||
matrix3: rotation,
|
||||
translation,
|
||||
});
|
||||
|
||||
let transform = Transform::from_scale(*nat_scale * normalizing_scale);
|
||||
|
||||
Self {
|
||||
name,
|
||||
marker,
|
||||
spatial: SpatialBundle {
|
||||
global_transform,
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
scale: nat_scale,
|
||||
}
|
||||
}
|
||||
TransformSpace::World => {
|
||||
// FIXME: I'm struggling to figure out how to eliminate the
|
||||
// "shear" from a fully inverted local transform when the
|
||||
// object is non-uniformly scaled. As a workaround, we'll
|
||||
// force these objects to operate in local-space mode.
|
||||
// TODO: If this turns into a long-term solution, update the UI
|
||||
// to reflect that world-space transformation is not
|
||||
// possible for this object (e.g., by disabling the
|
||||
// world-space toggle button, maybe with an explanatory
|
||||
// tooltip).
|
||||
let local_matrix = if nat_scale.is_non_uniform() {
|
||||
inv_scale
|
||||
} else {
|
||||
matrix3.inverse()
|
||||
};
|
||||
|
||||
let global_transform = GlobalTransform::from(Affine3A {
|
||||
matrix3,
|
||||
translation,
|
||||
});
|
||||
|
||||
let mat4 = Mat4::from_mat3a(local_matrix * normalizing_scale);
|
||||
let transform = Transform::from_matrix(mat4);
|
||||
|
||||
Self {
|
||||
name,
|
||||
marker,
|
||||
spatial: SpatialBundle {
|
||||
global_transform,
|
||||
transform,
|
||||
..default()
|
||||
},
|
||||
scale: nat_scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_gizmo_element_hover(
|
||||
ev: Listener<Pointer<Over>>,
|
||||
mut ra_mats: ResMut<Assets<StandardMaterial>>,
|
||||
q_target: Query<&Handle<StandardMaterial>>,
|
||||
) {
|
||||
let mat = ra_mats.get_mut(q_target.get(ev.target).unwrap()).unwrap();
|
||||
mat.base_color *= 2.;
|
||||
}
|
||||
|
||||
pub(super) fn on_gizmo_element_unhover(
|
||||
ev: Listener<Pointer<Out>>,
|
||||
mut ra_mats: ResMut<Assets<StandardMaterial>>,
|
||||
q_target: Query<&Handle<StandardMaterial>>,
|
||||
) {
|
||||
let mat = ra_mats.get_mut(q_target.get(ev.target).unwrap()).unwrap();
|
||||
mat.base_color *= 0.5;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
use bevy::{prelude::*, transform::TransformSystem};
|
||||
|
||||
use crate::{camera_controller::CameraMotionSystem, MainCamera};
|
||||
|
||||
use self::{
|
||||
camera::{spawn_gizmo_camera, GizmoCamera},
|
||||
combo::spawn_combo_translation_rotation_gizmo,
|
||||
common::*,
|
||||
resources::{init_resources, GizmoMats, GizmoMeshes},
|
||||
rotation::{spawn_rotation_gizmo, update_ss_ring_rotation},
|
||||
translation::spawn_translation_gizmo,
|
||||
};
|
||||
|
||||
use super::{Selection, TransformMode, TransformType};
|
||||
|
||||
mod camera;
|
||||
mod combo;
|
||||
mod common;
|
||||
mod resources;
|
||||
mod rotation;
|
||||
mod translation;
|
||||
|
||||
const GIZMO_RENDER_LAYER: u8 = 1;
|
||||
|
||||
/// (sin|cos)(pi / 4)
|
||||
const CIRC_45: f32 = 0.70710677;
|
||||
/// Mat3 ((0 -1 0) (1 0 0) (0 0 1))
|
||||
const ROT_X: Quat = Quat::from_xyzw(0., 0., CIRC_45, -CIRC_45);
|
||||
/// Mat3 ((1 0 0) (0 1 0) (0 0 1))
|
||||
const ROT_Y: Quat = Quat::IDENTITY;
|
||||
/// Mat3 ((1 0 0) (0 0 1) (0 -1 0))
|
||||
const ROT_Z: Quat = Quat::from_xyzw(CIRC_45, 0., 0., CIRC_45);
|
||||
|
||||
pub struct GizmoPlugin;
|
||||
|
||||
impl Plugin for GizmoPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<NaturalScale>();
|
||||
app.add_event::<RebuildGizmo>();
|
||||
app.add_systems(Startup, init_resources);
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
spawn_gizmo_camera.run_if(
|
||||
any_with_component::<MainCamera>()
|
||||
.and_then(not(any_with_component::<GizmoCamera>())),
|
||||
),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
despawn_existing_gizmo.run_if(
|
||||
resource_changed_or_removed::<Selection>()
|
||||
.or_else(resource_changed::<TransformMode>())
|
||||
.or_else(on_event::<RebuildGizmo>()),
|
||||
),
|
||||
spawn_gizmo.after(despawn_existing_gizmo).run_if(
|
||||
resource_exists::<Selection>()
|
||||
.and_then(not(any_with_component::<TransformGizmoRoot>())),
|
||||
),
|
||||
normalize_gizmo_scale.after(CameraMotionSystem),
|
||||
),
|
||||
);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
update_ss_ring_rotation.after(TransformSystem::TransformPropagate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn despawn_existing_gizmo(
|
||||
mut cmd: Commands,
|
||||
q_gizmo_root: Query<(Entity, &Children), With<TransformGizmoRoot>>,
|
||||
q_gizmos: Query<Entity, With<TransformGizmo>>,
|
||||
) {
|
||||
if let Ok((ent, children)) = q_gizmo_root.get_single() {
|
||||
if let Some(child) = children.iter().find_map(|ent| q_gizmos.get(*ent).ok()) {
|
||||
cmd.entity(child).despawn_recursive();
|
||||
}
|
||||
|
||||
cmd.entity(ent).remove::<TransformGizmoRoot>();
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_gizmo(
|
||||
mut cmd: Commands,
|
||||
r_selection: Res<Selection>,
|
||||
r_xform_mode: Res<TransformMode>,
|
||||
q_xform: Query<&GlobalTransform>,
|
||||
q_cam_xform: Query<&Transform, With<MainCamera>>,
|
||||
r_meshes: Res<GizmoMeshes>,
|
||||
r_mats: Res<GizmoMats>,
|
||||
) {
|
||||
let ent = **r_selection;
|
||||
let Ok(xform) = q_xform.get(ent) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(cam_xform) = q_cam_xform.get_single() else {
|
||||
return;
|
||||
};
|
||||
let dist = Vec3::distance(cam_xform.translation, xform.translation());
|
||||
let normalizing_scale = dist / 15.;
|
||||
|
||||
// TODO: Look into using the new one-shot systems feature here
|
||||
// https://bevyengine.org/news/bevy-0-12/#one-shot-systems
|
||||
match r_xform_mode.ttype {
|
||||
TransformType::TRANSLATE => {
|
||||
spawn_translation_gizmo(
|
||||
&mut cmd,
|
||||
ent,
|
||||
xform,
|
||||
r_xform_mode.space,
|
||||
normalizing_scale,
|
||||
&r_meshes,
|
||||
&r_mats,
|
||||
);
|
||||
}
|
||||
TransformType::ROTATE => {
|
||||
spawn_rotation_gizmo(
|
||||
&mut cmd,
|
||||
ent,
|
||||
xform,
|
||||
r_xform_mode.space,
|
||||
normalizing_scale,
|
||||
&r_meshes,
|
||||
&r_mats,
|
||||
);
|
||||
}
|
||||
ttype if ttype.contains(TransformType::TRANSLATE | TransformType::ROTATE) => {
|
||||
spawn_combo_translation_rotation_gizmo(
|
||||
&mut cmd,
|
||||
ent,
|
||||
xform,
|
||||
r_xform_mode.space,
|
||||
normalizing_scale,
|
||||
&r_meshes,
|
||||
&r_mats,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_gizmo_scale(
|
||||
q_cam_xform: Query<&Transform, With<MainCamera>>,
|
||||
mut q_gizmo: Query<
|
||||
(&mut Transform, &GlobalTransform, &NaturalScale),
|
||||
(With<TransformGizmo>, Without<MainCamera>),
|
||||
>,
|
||||
) {
|
||||
let Ok(cam_xform) = q_cam_xform.get_single() else {
|
||||
return;
|
||||
};
|
||||
let Ok((mut xform, world_xform, nat_scale)) = q_gizmo.get_single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dist = Vec3::distance(cam_xform.translation, world_xform.translation());
|
||||
xform.scale = **nat_scale * (dist / 15.);
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::{design_system, math::geo};
|
||||
|
||||
#[derive(Resource, Reflect, Debug, Default)]
|
||||
#[reflect(Resource)]
|
||||
pub(super) struct GizmoMeshes {
|
||||
pub translate_line: Handle<Mesh>,
|
||||
pub translate_arrow: Handle<Mesh>,
|
||||
pub translate_plane: Handle<Mesh>,
|
||||
pub translate_ss: Handle<Mesh>,
|
||||
pub rotate_ring: Handle<Mesh>,
|
||||
pub rotate_ball: Handle<Mesh>,
|
||||
pub rotate_ss: Handle<Mesh>,
|
||||
}
|
||||
|
||||
#[derive(Resource, Reflect, Debug, Default)]
|
||||
#[reflect(Resource)]
|
||||
pub(super) struct GizmoMats {
|
||||
pub translate_x: Handle<StandardMaterial>,
|
||||
pub translate_y: Handle<StandardMaterial>,
|
||||
pub translate_z: Handle<StandardMaterial>,
|
||||
pub translate_yz: Handle<StandardMaterial>,
|
||||
pub translate_xz: Handle<StandardMaterial>,
|
||||
pub translate_xy: Handle<StandardMaterial>,
|
||||
pub translate_ss: Handle<StandardMaterial>,
|
||||
pub rotate_x: Handle<StandardMaterial>,
|
||||
pub rotate_y: Handle<StandardMaterial>,
|
||||
pub rotate_z: Handle<StandardMaterial>,
|
||||
pub rotate_trackball: Handle<StandardMaterial>,
|
||||
pub rotate_ss: Handle<StandardMaterial>,
|
||||
}
|
||||
|
||||
pub(super) fn init_resources(
|
||||
mut cmd: Commands,
|
||||
mut ra_meshes: ResMut<Assets<Mesh>>,
|
||||
mut ra_mats: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
let translate_line = ra_meshes.add(Mesh::from(shape::Cylinder {
|
||||
radius: 0.03,
|
||||
height: 0.8,
|
||||
resolution: 6,
|
||||
segments: 1,
|
||||
}));
|
||||
let translate_arrow = ra_meshes.add(Mesh::from(geo::Cone {
|
||||
radius: 0.115,
|
||||
length: 0.4,
|
||||
resolution: 12,
|
||||
}));
|
||||
let translate_plane = ra_meshes.add(Mesh::from(geo::Plane2Sided { size: 0.25 }));
|
||||
let translate_ss = ra_meshes.add(
|
||||
Mesh::try_from(shape::Icosphere {
|
||||
radius: 0.2,
|
||||
subdivisions: 3,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let rotate_ring = ra_meshes.add(Mesh::from(shape::Torus {
|
||||
radius: 1.,
|
||||
ring_radius: 0.04,
|
||||
subdivisions_segments: 40,
|
||||
subdivisions_sides: 6,
|
||||
}));
|
||||
let rotate_ball = ra_meshes.add(
|
||||
Mesh::try_from(shape::Icosphere {
|
||||
radius: 0.96,
|
||||
subdivisions: 4,
|
||||
})
|
||||
.unwrap(),
|
||||
);
|
||||
let rotate_ss = ra_meshes.add(Mesh::from(shape::Torus {
|
||||
radius: 1.2,
|
||||
ring_radius: 0.03,
|
||||
subdivisions_segments: 40,
|
||||
subdivisions_sides: 6,
|
||||
}));
|
||||
|
||||
cmd.insert_resource(GizmoMeshes {
|
||||
translate_line,
|
||||
translate_arrow,
|
||||
translate_plane,
|
||||
translate_ss,
|
||||
rotate_ring,
|
||||
rotate_ball,
|
||||
rotate_ss,
|
||||
});
|
||||
|
||||
let translate_x = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::RED,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
let translate_yz = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::RED,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
|
||||
let translate_y = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::GREEN,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
let translate_xz = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::GREEN,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
|
||||
let translate_z = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::BLUE,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
let translate_xy = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::BLUE,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
|
||||
let translate_ss = ra_mats.add(StandardMaterial {
|
||||
base_color: Color::WHITE,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
|
||||
let rotate_x = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::RED,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
let rotate_y = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::GREEN,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
let rotate_z = ra_mats.add(StandardMaterial {
|
||||
base_color: design_system::Color::BLUE,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
let rotate_trackball = ra_mats.add(StandardMaterial {
|
||||
base_color: Color::NONE,
|
||||
emissive: Color::WHITE,
|
||||
alpha_mode: AlphaMode::Add,
|
||||
..default()
|
||||
});
|
||||
let rotate_ss = ra_mats.add(StandardMaterial {
|
||||
base_color: Color::WHITE,
|
||||
unlit: true,
|
||||
..default()
|
||||
});
|
||||
|
||||
cmd.insert_resource(GizmoMats {
|
||||
translate_x,
|
||||
translate_y,
|
||||
translate_z,
|
||||
translate_yz,
|
||||
translate_xz,
|
||||
translate_xy,
|
||||
translate_ss,
|
||||
rotate_x,
|
||||
rotate_y,
|
||||
rotate_z,
|
||||
rotate_trackball,
|
||||
rotate_ss,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
use bevy::{
|
||||
math::{vec3, Affine3A, DVec3, Mat3A, Vec3A},
|
||||
pbr::NotShadowCaster,
|
||||
prelude::*,
|
||||
render::view::RenderLayers,
|
||||
};
|
||||
use bevy_mod_picking::prelude::*;
|
||||
use bevy_mod_raycast::prelude::{Raycast, RaycastSettings};
|
||||
|
||||
use crate::{
|
||||
interaction::{TransformMode, TransformSpace},
|
||||
math::{MatrixExtras, NearlyZero},
|
||||
MainCamera,
|
||||
};
|
||||
|
||||
use super::{
|
||||
on_gizmo_element_hover, on_gizmo_element_unhover, GizmoMats, GizmoMeshes, InitialTransform,
|
||||
RebuildGizmo, TransformGizmo, TransformGizmoBundle, TransformGizmoRoot, GIZMO_RENDER_LAYER,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) enum RotationWidget {
|
||||
Axis(Vec3),
|
||||
ScreenSpace,
|
||||
Trackball,
|
||||
}
|
||||
|
||||
#[derive(Component, Deref)]
|
||||
pub(super) struct InitialHitPoint(pub Vec3);
|
||||
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub(super) struct LastHitPoint(pub Vec3);
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) struct LastRotation {
|
||||
pub axis: Vec3,
|
||||
pub theta: f32,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn spawn_rotation_gizmo(
|
||||
cmd: &mut Commands,
|
||||
ent: Entity,
|
||||
xform: &GlobalTransform,
|
||||
space: TransformSpace,
|
||||
normalizing_scale: f32,
|
||||
meshes: &GizmoMeshes,
|
||||
mats: &GizmoMats,
|
||||
) {
|
||||
cmd.entity(ent)
|
||||
.insert(TransformGizmoRoot)
|
||||
.with_children(|cmd| {
|
||||
cmd.spawn((
|
||||
TransformGizmoBundle::new(xform, space, normalizing_scale),
|
||||
On::<Pointer<DragStart>>::run(on_rotation_start),
|
||||
On::<Pointer<Drag>>::run(on_rotate),
|
||||
On::<Pointer<DragEnd>>::run(on_rotation_end),
|
||||
))
|
||||
.with_children(|cmd| {
|
||||
use super::{ROT_X, ROT_Y, ROT_Z};
|
||||
|
||||
// Trackball
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Trackball,
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ball.clone(),
|
||||
material: mats.rotate_trackball.clone(),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_trackball_hover),
|
||||
On::<Pointer<Out>>::run(on_trackball_unhover),
|
||||
));
|
||||
|
||||
// Screen-space
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::ScreenSpace,
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ss.clone(),
|
||||
material: mats.rotate_ss.clone(),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
));
|
||||
|
||||
// X
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Axis(Vec3::X),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ring.clone(),
|
||||
material: mats.rotate_x.clone(),
|
||||
transform: Transform::from_rotation(ROT_X),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
));
|
||||
// Y
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Axis(Vec3::Y),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ring.clone(),
|
||||
material: mats.rotate_y.clone(),
|
||||
transform: Transform::from_rotation(ROT_Y),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
));
|
||||
// Z
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
RotationWidget::Axis(Vec3::Z),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.rotate_ring.clone(),
|
||||
material: mats.rotate_z.clone(),
|
||||
transform: Transform::from_rotation(ROT_Z),
|
||||
..default()
|
||||
},
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn update_ss_ring_rotation(
|
||||
q_camera: Query<&GlobalTransform, With<MainCamera>>,
|
||||
mut q_ring: Query<(&mut GlobalTransform, &RotationWidget), Without<MainCamera>>,
|
||||
) {
|
||||
let Ok(cam_xform) = q_camera.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (mut world_xform, widget) in q_ring.iter_mut() {
|
||||
if matches!(*widget, RotationWidget::ScreenSpace) {
|
||||
// Find basis vector to rotate the flat side of the ring toward the camera
|
||||
let origin: Vec3A = world_xform.translation().into();
|
||||
let normal = (Vec3A::from(cam_xform.translation()) - origin).normalize();
|
||||
let (tangent, binormal) = normal.any_orthonormal_pair();
|
||||
let rotation = Mat3A::from_cols(tangent, normal, binormal);
|
||||
|
||||
// Since we're overriding the local transform propagation, we need
|
||||
// to manually re-apply the normalization scale
|
||||
let scale = world_xform.affine().matrix3.decompose_scale();
|
||||
|
||||
*world_xform = GlobalTransform::from(Affine3A {
|
||||
matrix3: rotation * scale,
|
||||
translation: origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_trackball_hover(
|
||||
ev: Listener<Pointer<Over>>,
|
||||
mut ra_mats: ResMut<Assets<StandardMaterial>>,
|
||||
q_target: Query<&Handle<StandardMaterial>>,
|
||||
) {
|
||||
let mat = ra_mats.get_mut(q_target.get(ev.target).unwrap()).unwrap();
|
||||
mat.base_color.set_a(0.2);
|
||||
}
|
||||
|
||||
fn on_trackball_unhover(
|
||||
ev: Listener<Pointer<Out>>,
|
||||
mut ra_mats: ResMut<Assets<StandardMaterial>>,
|
||||
q_target: Query<&Handle<StandardMaterial>>,
|
||||
) {
|
||||
let mat = ra_mats.get_mut(q_target.get(ev.target).unwrap()).unwrap();
|
||||
mat.base_color.set_a(0.);
|
||||
}
|
||||
|
||||
pub(super) fn on_rotation_start(
|
||||
event: Listener<Pointer<DragStart>>,
|
||||
mut cmd: Commands,
|
||||
q_xform: Query<(Entity, &GlobalTransform), With<TransformGizmoRoot>>,
|
||||
) {
|
||||
info!("Rotation start");
|
||||
|
||||
if let Ok((ent, &world_xform)) = q_xform.get_single() {
|
||||
let hit_point = event.hit.position.unwrap();
|
||||
|
||||
cmd.entity(ent).insert((
|
||||
InitialTransform(world_xform),
|
||||
InitialHitPoint(hit_point),
|
||||
LastHitPoint(hit_point),
|
||||
LastRotation {
|
||||
axis: Vec3::ZERO,
|
||||
theta: 0.,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_rotation_end(
|
||||
event: Listener<Pointer<DragEnd>>,
|
||||
mut cmd: Commands,
|
||||
r_xform_mode: Res<TransformMode>,
|
||||
mut ev_rebuild_gizmo: EventWriter<RebuildGizmo>,
|
||||
) {
|
||||
info!("Rotation end");
|
||||
|
||||
cmd.entity(event.target)
|
||||
.remove::<InitialTransform>()
|
||||
.remove::<InitialHitPoint>()
|
||||
.remove::<LastHitPoint>()
|
||||
.remove::<LastRotation>();
|
||||
|
||||
if matches!(r_xform_mode.space, TransformSpace::World) {
|
||||
ev_rebuild_gizmo.send(RebuildGizmo);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_rotate(
|
||||
event: Listener<Pointer<Drag>>,
|
||||
mut raycast: Raycast,
|
||||
mut q_root: Query<
|
||||
(
|
||||
&mut Transform,
|
||||
Option<&Parent>,
|
||||
&InitialTransform,
|
||||
&InitialHitPoint,
|
||||
&mut LastHitPoint,
|
||||
&mut LastRotation,
|
||||
),
|
||||
With<TransformGizmoRoot>,
|
||||
>,
|
||||
q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
|
||||
q_gizmo: Query<&GlobalTransform, (With<TransformGizmo>, Without<MainCamera>)>,
|
||||
q_world_xforms: Query<&GlobalTransform, (With<Children>, Without<MainCamera>)>,
|
||||
q_widget: Query<&RotationWidget>,
|
||||
) {
|
||||
// Components from the gizmo root (the entity being transformed)
|
||||
let (mut xform, parent, init_xform, init_point, mut last_point, mut last_rotation) =
|
||||
q_root.single_mut();
|
||||
|
||||
let gizmo_xform = q_gizmo.single();
|
||||
let (cam, cam_xform) = q_camera.single();
|
||||
|
||||
// `Transform::rotate_axis` takes an axis vector relative to the transform's
|
||||
// _parent_. We'll use the gizmo root's parent's global transform (if it
|
||||
// exists) to move the world-space axis of rotation into the parent space
|
||||
// when applying the rotation.
|
||||
let identity_xform = GlobalTransform::default();
|
||||
let root_parent_xform = parent
|
||||
.and_then(|parent| q_world_xforms.get(parent.get()).ok())
|
||||
.unwrap_or(&identity_xform);
|
||||
|
||||
// We compute the rotation by finding a triangle (in world-space) where:
|
||||
// p0 = the gizmo/object origin
|
||||
// p1,p2 = the intersections of a camera-to-world raycast, where p1 is the
|
||||
// previous pointer location and p2 is the current.
|
||||
let p1 = **last_point;
|
||||
let p0 = init_xform.translation();
|
||||
|
||||
// Find the screen-space p2 by adding the total drag distance to the pointer
|
||||
// location recorded on `DragStart`
|
||||
let p_init_ss = cam.world_to_viewport(cam_xform, **init_point).unwrap();
|
||||
let p2_ss = p_init_ss + event.distance;
|
||||
|
||||
// Create a camera-to-world ray that passes through that point. In trackball
|
||||
// mode, we'll trace this against the surface of the trackball sphere to
|
||||
// find the world-space point. In all other modes, we'll trace against a
|
||||
// plane whose origin is p0 and whose normal is the axis of the "ring"
|
||||
// that's being dragged.
|
||||
let ray = cam.viewport_to_world(cam_xform, p2_ss).unwrap();
|
||||
|
||||
use RotationWidget::*;
|
||||
|
||||
match q_widget.get(event.target) {
|
||||
Ok(Trackball) => {
|
||||
// Raycast against the trackball sphere to find p2
|
||||
let filter = |ent: Entity| ent == event.target;
|
||||
let early_exit_test = |ent: Entity| ent != event.target;
|
||||
let settings = RaycastSettings {
|
||||
filter: &filter,
|
||||
early_exit_test: &early_exit_test,
|
||||
..default()
|
||||
};
|
||||
let hits = raycast.cast_ray(ray.into(), &settings);
|
||||
|
||||
// If the pointer is no longer in the vicinity of the sphere, we'll
|
||||
// invoke this closure to extrapolate additional rotation from the
|
||||
// last recorded axis and angle
|
||||
let mut extrapolate = || {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
let axis_parent_space = last_rotation.axis;
|
||||
if axis_parent_space.nearly_zero() {
|
||||
return;
|
||||
}
|
||||
|
||||
let view_plane_normal = (cam_xform.translation() - p1).normalize();
|
||||
let hit_dist = ray.intersect_plane(p1, view_plane_normal).unwrap();
|
||||
let p2 = ray.origin + ray.direction * hit_dist;
|
||||
let drag_dist = (p2 - p1).length();
|
||||
// TODO: Sensitivity could use some tuning
|
||||
let theta = last_rotation.theta + drag_dist * PI * 0.5;
|
||||
|
||||
let mut init_xform_local = init_xform.reparented_to(root_parent_xform);
|
||||
init_xform_local.rotate_axis(axis_parent_space, theta);
|
||||
|
||||
*xform = init_xform_local;
|
||||
};
|
||||
|
||||
if hits.is_empty() {
|
||||
return extrapolate();
|
||||
}
|
||||
let (_, hit) = &hits[0];
|
||||
|
||||
// Use 64-bit precision for all of these vectors to derive an
|
||||
// accurate axis of rotation even at very small theta values
|
||||
let p0 = DVec3::from(p0);
|
||||
let p1 = DVec3::from(**init_point);
|
||||
let p2 = DVec3::from(hit.position());
|
||||
|
||||
let a = (p1 - p0).normalize_or_zero();
|
||||
let b = (p2 - p0).normalize_or_zero();
|
||||
if a.nearly_zero() || b.nearly_zero() {
|
||||
return extrapolate();
|
||||
}
|
||||
|
||||
// Find the angle @ p0
|
||||
let theta = a.dot(b).acos() as f32;
|
||||
|
||||
// Derive the axis of rotation by finding the vector perpendicular
|
||||
// to (p1-p0) and (p2-p0)
|
||||
let axis_ws = a.cross(b).normalize_or_zero();
|
||||
if axis_ws.nearly_zero() {
|
||||
return extrapolate();
|
||||
}
|
||||
|
||||
// Transform the world-space axis into the parent space
|
||||
let axis_parent_space = root_parent_xform
|
||||
.affine()
|
||||
.inverse()
|
||||
.transform_vector3(vec3(axis_ws.x as f32, axis_ws.y as f32, axis_ws.z as f32))
|
||||
.normalize();
|
||||
|
||||
// Apply the rotation and update our local state
|
||||
let mut init_xform_local = init_xform.reparented_to(root_parent_xform);
|
||||
init_xform_local.rotate_axis(axis_parent_space, theta);
|
||||
|
||||
*xform = init_xform_local;
|
||||
**last_point = vec3(p2.x as f32, p2.y as f32, p2.z as f32);
|
||||
last_rotation.axis = axis_parent_space;
|
||||
last_rotation.theta = theta;
|
||||
}
|
||||
Ok(axis) => {
|
||||
// Find the world-space axis of rotation
|
||||
let axis_ws = match axis {
|
||||
ScreenSpace => (p0 - cam_xform.translation()).normalize(),
|
||||
Axis(normal) => gizmo_xform.affine().transform_vector3(*normal).normalize(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Find the world-space p2, if possible
|
||||
let Some(hit_dist) = ray.intersect_plane(p0, axis_ws) else {
|
||||
// This could fail if the axis of rotation is nearly
|
||||
// perpendicular to the view direction
|
||||
return;
|
||||
};
|
||||
let p2 = ray.origin + ray.direction * hit_dist;
|
||||
|
||||
// Find the angle @ p0
|
||||
let a = p1 - p0;
|
||||
let b = p2 - p0;
|
||||
let theta = (a.dot(b) / (a.length() * b.length())).acos();
|
||||
if theta.is_nan() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the _direction_ of the rotation by comparing the canonical
|
||||
// axis of rotation with (p1-p0) x (p2-p0).
|
||||
//
|
||||
// In theory, we could just use that cross-product directly as our
|
||||
// axis of rotation, but floating-point errors at very small theta
|
||||
// values make that unfeasible in practice, so instead we just flip
|
||||
// the canonical axis when appropriate.
|
||||
let diff = axis_ws - a.cross(b).normalize();
|
||||
let axis_ws = if diff.length() > 1. {
|
||||
-axis_ws
|
||||
} else {
|
||||
axis_ws
|
||||
};
|
||||
|
||||
// Transform the world-space axis into the parent space
|
||||
let axis_parent_space = root_parent_xform
|
||||
.affine()
|
||||
.inverse()
|
||||
.transform_vector3(axis_ws)
|
||||
.normalize();
|
||||
|
||||
// Apply the rotation and update our local state
|
||||
xform.rotate_axis(axis_parent_space, theta);
|
||||
**last_point = p2;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
use bevy::{
|
||||
ecs::system::EntityCommands, math::vec3, pbr::NotShadowCaster, prelude::*,
|
||||
render::view::RenderLayers,
|
||||
};
|
||||
use bevy_mod_picking::prelude::{
|
||||
Drag, DragEnd, DragStart, Listener, ListenerInput, On, Out, Over, Pointer,
|
||||
};
|
||||
|
||||
use crate::{interaction::TransformSpace, MainCamera};
|
||||
|
||||
use super::{
|
||||
on_gizmo_element_hover, on_gizmo_element_unhover, GizmoMats, GizmoMeshes, InitialTransform,
|
||||
TransformGizmo, TransformGizmoBundle, TransformGizmoRoot, GIZMO_RENDER_LAYER,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) enum TranslationWidget {
|
||||
Axis(Vec3),
|
||||
Plane(Vec3),
|
||||
ScreenSpace,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn spawn_translation_gizmo(
|
||||
cmd: &mut Commands,
|
||||
ent: Entity,
|
||||
xform: &GlobalTransform,
|
||||
space: TransformSpace,
|
||||
normalizing_scale: f32,
|
||||
meshes: &GizmoMeshes,
|
||||
mats: &GizmoMats,
|
||||
) {
|
||||
cmd.entity(ent)
|
||||
.insert(TransformGizmoRoot)
|
||||
.with_children(|cmd| {
|
||||
cmd.spawn((
|
||||
TransformGizmoBundle::new(xform, space, normalizing_scale),
|
||||
On::<Pointer<Over>>::run(on_gizmo_element_hover),
|
||||
On::<Pointer<Out>>::run(on_gizmo_element_unhover),
|
||||
On::<Pointer<DragStart>>::run(on_translation_start),
|
||||
On::<Pointer<Drag>>::run(on_translate),
|
||||
On::<Pointer<DragEnd>>::target_commands_mut(on_translation_end),
|
||||
))
|
||||
.with_children(|cmd| {
|
||||
use super::{CIRC_45, ROT_X, ROT_Y, ROT_Z};
|
||||
|
||||
// Center sphere
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::ScreenSpace,
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_ss.clone(),
|
||||
material: mats.translate_ss.clone(),
|
||||
transform: Transform::IDENTITY,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// X
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::X),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_line.clone(),
|
||||
material: mats.translate_x.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_X,
|
||||
translation: vec3(0.6, 0., 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::X),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_arrow.clone(),
|
||||
material: mats.translate_x.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_X,
|
||||
translation: vec3(1., 0., 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Y
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::Y),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_line.clone(),
|
||||
material: mats.translate_y.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Y,
|
||||
translation: vec3(0., 0.6, 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::Y),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_arrow.clone(),
|
||||
material: mats.translate_y.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Y,
|
||||
translation: vec3(0., 1., 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// Z
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::Z),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_line.clone(),
|
||||
material: mats.translate_z.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Z,
|
||||
translation: vec3(0., 0., 0.6),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Axis(Vec3::Z),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_arrow.clone(),
|
||||
material: mats.translate_z.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Z,
|
||||
translation: vec3(0., 0., 1.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
|
||||
// YZ Plane
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Plane(Vec3::X),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_plane.clone(),
|
||||
material: mats.translate_yz.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_X,
|
||||
translation: vec3(0., CIRC_45, CIRC_45),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
// XZ Plane
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Plane(Vec3::Y),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_plane.clone(),
|
||||
material: mats.translate_xz.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Y,
|
||||
translation: vec3(CIRC_45, 0., CIRC_45),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
// XY Plane
|
||||
cmd.spawn((
|
||||
NotShadowCaster,
|
||||
RenderLayers::layer(GIZMO_RENDER_LAYER),
|
||||
TranslationWidget::Plane(Vec3::Z),
|
||||
MaterialMeshBundle {
|
||||
mesh: meshes.translate_plane.clone(),
|
||||
material: mats.translate_xy.clone(),
|
||||
transform: Transform {
|
||||
rotation: ROT_Z,
|
||||
translation: vec3(CIRC_45, CIRC_45, 0.),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn on_translation_start(
|
||||
_: Listener<Pointer<DragStart>>,
|
||||
mut cmd: Commands,
|
||||
q_xform: Query<(Entity, &GlobalTransform), With<TransformGizmoRoot>>,
|
||||
) {
|
||||
if let Ok((ent, &world_xform)) = q_xform.get_single() {
|
||||
cmd.entity(ent).insert(InitialTransform(world_xform));
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) fn on_translation_end(
|
||||
_: &ListenerInput<Pointer<DragEnd>>,
|
||||
cmd: &mut EntityCommands<'_, '_, '_>,
|
||||
) {
|
||||
cmd.remove::<InitialTransform>();
|
||||
}
|
||||
|
||||
pub(super) fn on_translate(
|
||||
event: Listener<Pointer<Drag>>,
|
||||
mut q_root: Query<(&mut Transform, &InitialTransform), With<TransformGizmoRoot>>,
|
||||
q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
|
||||
q_gizmo: Query<&GlobalTransform, (With<TransformGizmo>, Without<MainCamera>)>,
|
||||
q_widget: Query<&TranslationWidget>,
|
||||
) {
|
||||
let (mut xform, init_xform) = q_root.single_mut();
|
||||
let gizmo_xform = q_gizmo.single();
|
||||
let (cam, cam_xform) = q_camera.single();
|
||||
|
||||
// Find the initial origin point in screen-space and add the total screen-space drag distance
|
||||
let origin_ws = init_xform.translation();
|
||||
let origin_ss = cam.world_to_viewport(cam_xform, origin_ws).unwrap();
|
||||
let target_ss = origin_ss + event.distance;
|
||||
|
||||
// Create a camera-to-world ray that passes through the screen-space target point
|
||||
let ray = cam.viewport_to_world(cam_xform, target_ss).unwrap();
|
||||
|
||||
use TranslationWidget::*;
|
||||
|
||||
match q_widget.get(event.target) {
|
||||
Ok(Axis(axis)) => {
|
||||
// Transform the translation axis into world-space
|
||||
let axis_ws = gizmo_xform.affine().transform_vector3(*axis).normalize();
|
||||
|
||||
// Find the plane vector that's orthogonal to the translation axis
|
||||
// and closest to parallel with the view direction
|
||||
let axis_tan = axis_ws.cross(cam_xform.back());
|
||||
let plane_vector = axis_ws.cross(axis_tan);
|
||||
|
||||
// Find the intersection of the camera-to-world ray against that plane
|
||||
let Some(hit_dist) = ray.intersect_plane(origin_ws, plane_vector) else {
|
||||
return;
|
||||
};
|
||||
let hit_point = ray.origin + ray.direction * hit_dist;
|
||||
|
||||
// Project the vector from origin -> hit point onto the translation
|
||||
// axis to constrain the translation
|
||||
let drag_vector = hit_point - origin_ws;
|
||||
let projection_length = drag_vector.dot(axis_ws);
|
||||
let target_ws = origin_ws + axis_ws * projection_length;
|
||||
|
||||
xform.translation = target_ws;
|
||||
}
|
||||
Ok(plane) => {
|
||||
// Find the world-space normal of the plane we're translating on
|
||||
let plane_normal = match plane {
|
||||
ScreenSpace => cam_xform.back(),
|
||||
Plane(normal) => gizmo_xform.affine().transform_vector3(*normal),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Find the intersection of the camera-to-world ray against that plane
|
||||
let Some(hit_dist) = ray.intersect_plane(origin_ws, plane_normal) else {
|
||||
return;
|
||||
};
|
||||
let target_ws = ray.origin + ray.direction * hit_dist;
|
||||
|
||||
xform.translation = target_ws;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
mod gizmos;
|
||||
pub mod selection;
|
||||
pub mod transformation;
|
||||
|
||||
pub use selection::*;
|
||||
pub use transformation::*;
|
||||
|
||||
pub struct InteractionPlugin;
|
||||
impl Plugin for InteractionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((SelectionPlugin, TransformationPlugin));
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,19 @@ use bevy_mod_outline::{
|
||||
};
|
||||
use bevy_mod_picking::{prelude::Pointer, selection::Select};
|
||||
|
||||
use crate::{
|
||||
camera_controller::CameraMotionSystem,
|
||||
debug,
|
||||
input::{self, Action, InputAction, Lifecycle},
|
||||
};
|
||||
use crate::input::{self, Action, InputAction, Lifecycle};
|
||||
|
||||
pub struct SelectionPlugin;
|
||||
impl Plugin for SelectionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((OutlinePlugin, AutoGenerateOutlineNormalsPlugin))
|
||||
.add_systems(Update, update_selection)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Copy)]
|
||||
pub struct Selectable;
|
||||
@@ -19,24 +27,6 @@ pub struct Selected;
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct Selection(Entity);
|
||||
|
||||
pub struct InteractionPlugin;
|
||||
impl Plugin for InteractionPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins((OutlinePlugin, AutoGenerateOutlineNormalsPlugin));
|
||||
app.add_systems(Update, update_selection);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
|
||||
draw_selection_axes
|
||||
.run_if(resource_exists::<Selection>())
|
||||
.run_if(debug::is_debug_mode)
|
||||
.after(CameraMotionSystem),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_selection(
|
||||
mut cmd: Commands,
|
||||
// FIXME: These events aren't aware of our input actions, so we receive a
|
||||
@@ -89,27 +79,6 @@ fn update_selection(
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_selection_axes(
|
||||
mut gizmos: Gizmos,
|
||||
selection: Res<Selection>,
|
||||
q_transform: Query<&GlobalTransform>,
|
||||
) {
|
||||
let Ok(xform) = q_transform.get(**selection) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let origin = xform.translation();
|
||||
|
||||
let mut draw_axis = |direction: Vec3, color: Color| {
|
||||
let end = origin + direction * 0.25;
|
||||
gizmos.line(origin, end, color);
|
||||
};
|
||||
|
||||
draw_axis(xform.right(), Color::RED);
|
||||
draw_axis(xform.up(), Color::GREEN);
|
||||
draw_axis(xform.back(), Color::BLUE);
|
||||
}
|
||||
|
||||
fn draw_selection_outline(
|
||||
mut cmd: Commands,
|
||||
q_selected: Query<&Children, With<Selected>>,
|
||||
@@ -0,0 +1,132 @@
|
||||
use core::fmt;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bitflags::bitflags;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use super::gizmos::GizmoPlugin;
|
||||
|
||||
pub struct TransformationPlugin;
|
||||
|
||||
impl Plugin for TransformationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(TransformMode::default());
|
||||
app.add_plugins(GizmoPlugin);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
app.add_systems(PreUpdate, wasm::update_transform_mode);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct TransformMode {
|
||||
pub ttype: TransformType,
|
||||
pub space: TransformSpace,
|
||||
}
|
||||
|
||||
impl Default for TransformMode {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ttype: TransformType::TRANSLATE,
|
||||
space: default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[wasm_bindgen]
|
||||
pub enum TransformSpace {
|
||||
#[default]
|
||||
Local,
|
||||
World,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TransformType : u8 {
|
||||
const TRANSLATE = 0b001;
|
||||
const ROTATE = 0b010;
|
||||
const SCALE = 0b100;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TransformType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "TransformType(")?;
|
||||
|
||||
let mut written = false;
|
||||
if self.contains(TransformType::TRANSLATE) {
|
||||
if written {
|
||||
write!(f, " | ")?;
|
||||
}
|
||||
write!(f, "TRANSLATE")?;
|
||||
written = true;
|
||||
}
|
||||
if self.contains(TransformType::ROTATE) {
|
||||
if written {
|
||||
write!(f, " | ")?;
|
||||
}
|
||||
write!(f, "ROTATE")?;
|
||||
written = true;
|
||||
}
|
||||
if self.contains(TransformType::SCALE) {
|
||||
if written {
|
||||
write!(f, " | ")?;
|
||||
}
|
||||
write!(f, "SCALE")?;
|
||||
}
|
||||
|
||||
if self.bits() == 0 {
|
||||
write!(f, "None")?;
|
||||
}
|
||||
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm {
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{self, WasmMessageBuffer};
|
||||
|
||||
use super::{TransformMode, TransformSpace, TransformType};
|
||||
|
||||
#[wasm_bindgen(js_name = TransformType)]
|
||||
#[rustfmt::skip]
|
||||
pub enum JsTransformType {
|
||||
Translate = 0b001,
|
||||
Rotate = 0b010,
|
||||
Scale = 0b100,
|
||||
}
|
||||
|
||||
impl From<u8> for TransformType {
|
||||
fn from(value: u8) -> Self {
|
||||
TransformType::from_bits_retain(value)
|
||||
}
|
||||
}
|
||||
|
||||
static TRANSFORM_TYPE_BUF: WasmMessageBuffer<TransformType> = WasmMessageBuffer::new();
|
||||
static TRANSFORM_SPACE_BUF: WasmMessageBuffer<TransformSpace> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = setTransformType)]
|
||||
pub fn set_transform_type(flags: u8) {
|
||||
wasm::write_message!(TRANSFORM_TYPE_BUF, flags.into());
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = setTransformSpace)]
|
||||
pub fn set_transform_space(space: TransformSpace) {
|
||||
wasm::write_message!(TRANSFORM_SPACE_BUF, space);
|
||||
}
|
||||
|
||||
pub(super) fn update_transform_mode(mut xform_mode: ResMut<TransformMode>) {
|
||||
wasm::read_buffer!(TRANSFORM_TYPE_BUF, |ttype| {
|
||||
info!("Setting transform type: {ttype:?}");
|
||||
xform_mode.ttype = ttype;
|
||||
});
|
||||
wasm::read_buffer!(TRANSFORM_SPACE_BUF, |space| {
|
||||
info!("Setting transform space: {space:?}");
|
||||
xform_mode.space = space;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
mod camera_controller;
|
||||
mod debug;
|
||||
mod design_system;
|
||||
mod input;
|
||||
mod interaction;
|
||||
mod math;
|
||||
@@ -23,6 +24,12 @@ mod skybox;
|
||||
mod ui;
|
||||
mod viewport;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
|
||||
#[derive(Component)]
|
||||
pub(crate) struct MainCamera;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn init(viewport_size: ViewportSize) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Helper traits for floating-point equality checks with configurable tolerance.
|
||||
|
||||
use bevy::prelude::Vec3;
|
||||
use bevy::{math::DVec3, prelude::Vec3};
|
||||
|
||||
use super::internal::Zero;
|
||||
|
||||
@@ -53,6 +53,23 @@ impl Zero for Vec3 {
|
||||
}
|
||||
}
|
||||
|
||||
impl NearlyEq<DVec3, f64> for DVec3 {
|
||||
const DEFAULT_TOLERANCE: f64 = <f64 as NearlyEq<f64>>::DEFAULT_TOLERANCE;
|
||||
|
||||
fn nearly_eq_within(self, rhs: DVec3, tolerance: f64) -> bool {
|
||||
self.x.nearly_eq_within(rhs.x, tolerance)
|
||||
&& self.y.nearly_eq_within(rhs.y, tolerance)
|
||||
&& self.z.nearly_eq_within(rhs.z, tolerance)
|
||||
}
|
||||
}
|
||||
|
||||
impl Zero for DVec3 {
|
||||
#[inline]
|
||||
fn zero() -> Self {
|
||||
DVec3::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NearlyZero<Tolerance = Self> {
|
||||
const DEFAULT_TOLERANCE: Tolerance;
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
use bevy::{
|
||||
math::vec3,
|
||||
prelude::*,
|
||||
render::{mesh::Indices, render_resource::PrimitiveTopology},
|
||||
};
|
||||
|
||||
pub struct Cone {
|
||||
pub radius: f32,
|
||||
pub length: f32,
|
||||
pub resolution: u32,
|
||||
}
|
||||
|
||||
impl From<Cone> for Mesh {
|
||||
fn from(c: Cone) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
let num_verts = c.resolution + 2;
|
||||
let num_tris = c.resolution * 2;
|
||||
let num_indices = num_tris * 3;
|
||||
|
||||
let mut positions = Vec::with_capacity(num_verts as usize);
|
||||
let mut indices = Vec::with_capacity(num_indices as usize);
|
||||
|
||||
let rotator = Mat3::from_axis_angle(Vec3::Y, PI * 2. / c.resolution as f32);
|
||||
|
||||
positions.push(Vec3::ZERO);
|
||||
|
||||
let v_base = 0;
|
||||
let v_tip = num_verts - 1;
|
||||
|
||||
let mut vert = vec3(0., 0., c.radius);
|
||||
for i in 0..c.resolution {
|
||||
positions.push(vert);
|
||||
vert = rotator * vert;
|
||||
|
||||
let v1 = i + 1;
|
||||
let v2 = ((i + 2) % (c.resolution + 1)).max(1);
|
||||
|
||||
#[rustfmt::skip]
|
||||
indices.extend([
|
||||
v2, v1, v_base,
|
||||
v1, v2, v_tip,
|
||||
]);
|
||||
}
|
||||
|
||||
positions.push(vec3(0., c.length, 0.));
|
||||
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList);
|
||||
mesh.set_indices(Some(Indices::U32(indices)));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
|
||||
mesh
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Plane2Sided {
|
||||
pub size: f32,
|
||||
}
|
||||
|
||||
impl Default for Plane2Sided {
|
||||
fn default() -> Self {
|
||||
Self { size: 1. }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Plane2Sided> for Mesh {
|
||||
fn from(p: Plane2Sided) -> Self {
|
||||
let offset = p.size / 2.;
|
||||
|
||||
// [x][ ][x]
|
||||
// [ ][ ][ ]
|
||||
// [x][ ][x]
|
||||
#[rustfmt::skip]
|
||||
let positions = vec![
|
||||
// 0:
|
||||
// [x][ ][ ]
|
||||
// [ ][ ][ ]
|
||||
// [ ][ ][ ]
|
||||
vec3(-offset, 0., -offset),
|
||||
// 1:
|
||||
// [ ][ ][x]
|
||||
// [ ][ ][ ]
|
||||
// [ ][ ][ ]
|
||||
vec3( offset, 0., -offset),
|
||||
// 2:
|
||||
// [ ][ ][ ]
|
||||
// [ ][ ][ ]
|
||||
// [ ][ ][x]
|
||||
vec3( offset, 0., offset),
|
||||
// 3:
|
||||
// [ ][ ][ ]
|
||||
// [ ][ ][ ]
|
||||
// [x][ ][ ]
|
||||
vec3(-offset, 0., offset),
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
let indices = vec![
|
||||
// Top side
|
||||
0, 3, 1,
|
||||
1, 3, 2,
|
||||
// Bottom side
|
||||
1, 3, 0,
|
||||
2, 3, 1,
|
||||
];
|
||||
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList);
|
||||
mesh.set_indices(Some(Indices::U32(indices)));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||
|
||||
mesh
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use bevy::{
|
||||
math::{vec3, vec3a, Mat3A, Vec3A},
|
||||
prelude::{Mat3, Vec3},
|
||||
};
|
||||
|
||||
pub trait MatrixExtras
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn decompose_scale(&self) -> Self;
|
||||
fn decompose_scale_rotation(&self) -> (Self, Self);
|
||||
}
|
||||
|
||||
pub trait VecExtras
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn from_scale_mat(matrix: Mat3A) -> Self;
|
||||
}
|
||||
|
||||
impl MatrixExtras for Mat3A {
|
||||
#[inline]
|
||||
fn decompose_scale(&self) -> Mat3A {
|
||||
let det = self.determinant();
|
||||
let scale = Vec3A::new(
|
||||
self.x_axis.length() * f32::signum(det),
|
||||
self.y_axis.length(),
|
||||
self.z_axis.length(),
|
||||
);
|
||||
|
||||
Mat3A::from_cols(
|
||||
vec3a(scale.x, 0., 0.),
|
||||
vec3a(0., scale.y, 0.),
|
||||
vec3a(0., 0., scale.z),
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decompose_scale_rotation(&self) -> (Mat3A, Mat3A) {
|
||||
let scale_mat = self.decompose_scale();
|
||||
let scale = Vec3A::from_scale_mat(scale_mat);
|
||||
let inv_scale = scale.recip();
|
||||
|
||||
let rotation = Mat3A::from_cols(
|
||||
self.x_axis * inv_scale.x,
|
||||
self.y_axis * inv_scale.y,
|
||||
self.z_axis * inv_scale.z,
|
||||
);
|
||||
|
||||
(scale_mat, rotation)
|
||||
}
|
||||
}
|
||||
|
||||
impl MatrixExtras for Mat3 {
|
||||
#[inline]
|
||||
fn decompose_scale(&self) -> Self {
|
||||
Mat3A::from(*self).decompose_scale().into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn decompose_scale_rotation(&self) -> (Mat3, Mat3) {
|
||||
let (scale, rotation) = Mat3A::from(*self).decompose_scale_rotation();
|
||||
(scale.into(), rotation.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl VecExtras for Vec3A {
|
||||
#[inline]
|
||||
fn from_scale_mat(matrix: Mat3A) -> Self {
|
||||
vec3a(matrix.x_axis.x, matrix.y_axis.y, matrix.z_axis.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl VecExtras for Vec3 {
|
||||
#[inline]
|
||||
fn from_scale_mat(matrix: Mat3A) -> Self {
|
||||
vec3(matrix.x_axis.x, matrix.y_axis.y, matrix.z_axis.z)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,22 @@
|
||||
use bevy::prelude::Vec3;
|
||||
|
||||
mod float_comparison;
|
||||
pub mod geo;
|
||||
mod internal;
|
||||
pub mod interp;
|
||||
pub mod matrix;
|
||||
|
||||
pub use float_comparison::*;
|
||||
pub use geo::*;
|
||||
pub use interp::*;
|
||||
pub use matrix::*;
|
||||
|
||||
pub trait IsNonUniform {
|
||||
fn is_non_uniform(&self) -> bool;
|
||||
}
|
||||
|
||||
impl IsNonUniform for Vec3 {
|
||||
fn is_non_uniform(&self) -> bool {
|
||||
!self.x.nearly_eq_within(self.y, 1e-3) || !self.x.nearly_eq_within(self.z, 1e-3)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -19,6 +19,7 @@ use crate::{
|
||||
camera_controller::{CameraController, CameraControllerBundle, CameraMotionSystem},
|
||||
interaction::Selectable,
|
||||
skybox::SkyboxId,
|
||||
MainCamera,
|
||||
};
|
||||
|
||||
pub struct ScenePlugin;
|
||||
@@ -50,8 +51,8 @@ struct NewGltf(pub Handle<Gltf>);
|
||||
fn initialize(
|
||||
mut cmd: Commands,
|
||||
assets: Res<AssetServer>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
|
||||
mut ra_meshes: ResMut<Assets<Mesh>>,
|
||||
mut ra_mat_proc_grid: ResMut<Assets<ProceduralGridMaterial>>,
|
||||
) {
|
||||
// Skybox
|
||||
cmd.insert_resource(SkyboxId("test_scene".into()));
|
||||
@@ -71,6 +72,7 @@ fn initialize(
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
@@ -127,11 +129,11 @@ fn initialize(
|
||||
));
|
||||
|
||||
// Plane
|
||||
let plane = meshes.add(Mesh::from(shape::Plane {
|
||||
let plane = ra_meshes.add(Mesh::from(shape::Plane {
|
||||
size: 200.0,
|
||||
subdivisions: 0,
|
||||
}));
|
||||
let proc_grid = mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
let proc_grid = ra_mat_proc_grid.add(ProceduralGridMaterial {});
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("World Grid"),
|
||||
|
||||
@@ -7,6 +7,8 @@ use bevy::{
|
||||
render::render_resource::{TextureViewDescriptor, TextureViewDimension},
|
||||
};
|
||||
|
||||
use crate::MainCamera;
|
||||
|
||||
pub struct SkyboxPlugin;
|
||||
|
||||
impl Plugin for SkyboxPlugin {
|
||||
@@ -59,7 +61,7 @@ fn spawn_skybox(
|
||||
mut cmd: Commands,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
skybox_assets: Res<SkyboxAssets>,
|
||||
q_camera: Query<Entity, With<Camera3d>>,
|
||||
q_camera: Query<Entity, With<MainCamera>>,
|
||||
) {
|
||||
// Configure the skybox texture as a cubemap
|
||||
let image = images.get_mut(&skybox_assets.cubemap).unwrap();
|
||||
|
||||
+7
-14
@@ -47,22 +47,18 @@ impl fmt::Debug for ViewportSize {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm {
|
||||
use std::{cell::RefCell, sync::OnceLock};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use parking_lot::ReentrantMutex;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{self, WasmMessageBuffer};
|
||||
|
||||
use super::ViewportSize;
|
||||
|
||||
static RESIZE_BUF: OnceLock<ReentrantMutex<RefCell<Option<ViewportSize>>>> = OnceLock::new();
|
||||
static RESIZE_BUF: WasmMessageBuffer<ViewportSize> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn resize(viewport_size: ViewportSize) {
|
||||
let buffer = RESIZE_BUF.get_or_init(|| ReentrantMutex::new(RefCell::new(None)));
|
||||
if let Some(lock) = buffer.try_lock() {
|
||||
*lock.borrow_mut() = Some(viewport_size);
|
||||
}
|
||||
wasm::write_message!(RESIZE_BUF, viewport_size);
|
||||
}
|
||||
|
||||
pub struct ViewportPlugin;
|
||||
@@ -89,11 +85,8 @@ pub mod wasm {
|
||||
}
|
||||
|
||||
fn dispatch_resize_events(mut events: EventWriter<ViewportResizeEvent>) {
|
||||
let buffer = RESIZE_BUF.get_or_init(|| ReentrantMutex::new(RefCell::new(None)));
|
||||
if let Some(lock) = buffer.try_lock() {
|
||||
if let Some(resize) = lock.borrow_mut().take() {
|
||||
events.send(ViewportResizeEvent(resize));
|
||||
}
|
||||
}
|
||||
wasm::read_buffer!(RESIZE_BUF, |resize| {
|
||||
events.send(ViewportResizeEvent(resize));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
use std::{cell::RefCell, sync::OnceLock};
|
||||
|
||||
use parking_lot::ReentrantMutex;
|
||||
|
||||
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
|
||||
|
||||
macro_rules! get_buffer {
|
||||
($buffer:ident) => {
|
||||
$buffer.get_or_init(|| {
|
||||
::parking_lot::ReentrantMutex::new(::std::cell::RefCell::new(
|
||||
::core::option::Option::None,
|
||||
))
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! write_message {
|
||||
($buffer:ident, $expr:expr) => {{
|
||||
let buffer = crate::wasm::get_buffer!($buffer);
|
||||
if let Some(lock) = buffer.try_lock() {
|
||||
*lock.borrow_mut() = Some($expr);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! read_buffer {
|
||||
($buffer:ident, |$msg:ident| $stmt:stmt) => {{
|
||||
let buffer = crate::wasm::get_buffer!($buffer);
|
||||
if let Some(lock) = buffer.try_lock() {
|
||||
if let Some($msg) = lock.borrow_mut().take() {
|
||||
$stmt
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use get_buffer;
|
||||
pub(crate) use read_buffer;
|
||||
pub(crate) use write_message;
|
||||
Reference in New Issue
Block a user