diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml
index 58091d9fab..6d0fe810b2 100644
--- a/interface/resources/qml/hifi/tablet/TabletButton.qml
+++ b/interface/resources/qml/hifi/tablet/TabletButton.qml
@@ -84,7 +84,7 @@ Item {
}
function urlHelper(src) {
- if (src.match(/\bhttp/)) {
+ if (src.match(/\bhttp/) || src.match(/\bfile:/)) {
return src;
} else {
return "../../../" + src;
diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml
index bbf2d019fb..63149ad23b 100644
--- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml
+++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml
@@ -34,7 +34,7 @@ StateImage {
}
function urlHelper(src) {
- if (src.match(/\bhttp/)) {
+ if (src.match(/\bhttp/) || src.match(/\bfile:/)) {
return src;
} else {
return "../../../" + src;
diff --git a/scripts/shapes/assets/audio/clone.wav b/scripts/shapes/assets/audio/clone.wav
new file mode 100644
index 0000000000..2a0e21cdd2
Binary files /dev/null and b/scripts/shapes/assets/audio/clone.wav differ
diff --git a/scripts/shapes/assets/audio/create.wav b/scripts/shapes/assets/audio/create.wav
new file mode 100644
index 0000000000..9571c5cff7
Binary files /dev/null and b/scripts/shapes/assets/audio/create.wav differ
diff --git a/scripts/shapes/assets/audio/delete.wav b/scripts/shapes/assets/audio/delete.wav
new file mode 100644
index 0000000000..da93ca7e10
Binary files /dev/null and b/scripts/shapes/assets/audio/delete.wav differ
diff --git a/scripts/shapes/assets/audio/drop.wav b/scripts/shapes/assets/audio/drop.wav
new file mode 100644
index 0000000000..32d0e9f65a
Binary files /dev/null and b/scripts/shapes/assets/audio/drop.wav differ
diff --git a/scripts/shapes/assets/audio/equip.wav b/scripts/shapes/assets/audio/equip.wav
new file mode 100644
index 0000000000..32d0e9f65a
Binary files /dev/null and b/scripts/shapes/assets/audio/equip.wav differ
diff --git a/scripts/shapes/assets/audio/error.wav b/scripts/shapes/assets/audio/error.wav
new file mode 100644
index 0000000000..934a4d366b
Binary files /dev/null and b/scripts/shapes/assets/audio/error.wav differ
diff --git a/scripts/shapes/assets/audio/select.wav b/scripts/shapes/assets/audio/select.wav
new file mode 100644
index 0000000000..e97b5ea6d7
Binary files /dev/null and b/scripts/shapes/assets/audio/select.wav differ
diff --git a/scripts/shapes/assets/blue-header-bar.fbx b/scripts/shapes/assets/blue-header-bar.fbx
new file mode 100644
index 0000000000..951a6667e7
Binary files /dev/null and b/scripts/shapes/assets/blue-header-bar.fbx differ
diff --git a/scripts/shapes/assets/create/circle.fbx b/scripts/shapes/assets/create/circle.fbx
new file mode 100644
index 0000000000..db43fafab6
Binary files /dev/null and b/scripts/shapes/assets/create/circle.fbx differ
diff --git a/scripts/shapes/assets/create/cone.fbx b/scripts/shapes/assets/create/cone.fbx
new file mode 100644
index 0000000000..d590ee04be
Binary files /dev/null and b/scripts/shapes/assets/create/cone.fbx differ
diff --git a/scripts/shapes/assets/create/create-heading.svg b/scripts/shapes/assets/create/create-heading.svg
new file mode 100644
index 0000000000..fa2a096ede
--- /dev/null
+++ b/scripts/shapes/assets/create/create-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/create/cube.fbx b/scripts/shapes/assets/create/cube.fbx
new file mode 100644
index 0000000000..fa079219fe
Binary files /dev/null and b/scripts/shapes/assets/create/cube.fbx differ
diff --git a/scripts/shapes/assets/create/cylinder.fbx b/scripts/shapes/assets/create/cylinder.fbx
new file mode 100644
index 0000000000..319cbf0a6d
Binary files /dev/null and b/scripts/shapes/assets/create/cylinder.fbx differ
diff --git a/scripts/shapes/assets/create/dodecahedron.fbx b/scripts/shapes/assets/create/dodecahedron.fbx
new file mode 100644
index 0000000000..af2b7f3f55
Binary files /dev/null and b/scripts/shapes/assets/create/dodecahedron.fbx differ
diff --git a/scripts/shapes/assets/create/hexagon.fbx b/scripts/shapes/assets/create/hexagon.fbx
new file mode 100644
index 0000000000..ffb63fb64d
Binary files /dev/null and b/scripts/shapes/assets/create/hexagon.fbx differ
diff --git a/scripts/shapes/assets/create/icosahedron.fbx b/scripts/shapes/assets/create/icosahedron.fbx
new file mode 100644
index 0000000000..3103d6b2d6
Binary files /dev/null and b/scripts/shapes/assets/create/icosahedron.fbx differ
diff --git a/scripts/shapes/assets/create/octagon.fbx b/scripts/shapes/assets/create/octagon.fbx
new file mode 100644
index 0000000000..8de0f6f6f3
Binary files /dev/null and b/scripts/shapes/assets/create/octagon.fbx differ
diff --git a/scripts/shapes/assets/create/octahedron.fbx b/scripts/shapes/assets/create/octahedron.fbx
new file mode 100644
index 0000000000..6798566d3b
Binary files /dev/null and b/scripts/shapes/assets/create/octahedron.fbx differ
diff --git a/scripts/shapes/assets/create/prism.fbx b/scripts/shapes/assets/create/prism.fbx
new file mode 100644
index 0000000000..f1ddc4933e
Binary files /dev/null and b/scripts/shapes/assets/create/prism.fbx differ
diff --git a/scripts/shapes/assets/create/sphere.fbx b/scripts/shapes/assets/create/sphere.fbx
new file mode 100644
index 0000000000..43b077b225
Binary files /dev/null and b/scripts/shapes/assets/create/sphere.fbx differ
diff --git a/scripts/shapes/assets/create/tetrahedron.fbx b/scripts/shapes/assets/create/tetrahedron.fbx
new file mode 100644
index 0000000000..60dd70edb1
Binary files /dev/null and b/scripts/shapes/assets/create/tetrahedron.fbx differ
diff --git a/scripts/shapes/assets/gray-header.fbx b/scripts/shapes/assets/gray-header.fbx
new file mode 100644
index 0000000000..0f51d66f38
Binary files /dev/null and b/scripts/shapes/assets/gray-header.fbx differ
diff --git a/scripts/shapes/assets/green-header-bar.fbx b/scripts/shapes/assets/green-header-bar.fbx
new file mode 100644
index 0000000000..cf76830d8c
Binary files /dev/null and b/scripts/shapes/assets/green-header-bar.fbx differ
diff --git a/scripts/shapes/assets/green-header.fbx b/scripts/shapes/assets/green-header.fbx
new file mode 100644
index 0000000000..db77ecf0bd
Binary files /dev/null and b/scripts/shapes/assets/green-header.fbx differ
diff --git a/scripts/shapes/assets/horizontal-rule.svg b/scripts/shapes/assets/horizontal-rule.svg
new file mode 100644
index 0000000000..d7dc41f719
--- /dev/null
+++ b/scripts/shapes/assets/horizontal-rule.svg
@@ -0,0 +1,66 @@
+
+
+
+
diff --git a/scripts/shapes/assets/shapes-a.svg b/scripts/shapes/assets/shapes-a.svg
new file mode 100644
index 0000000000..07918ba294
--- /dev/null
+++ b/scripts/shapes/assets/shapes-a.svg
@@ -0,0 +1,15 @@
+
+
+
diff --git a/scripts/shapes/assets/shapes-d.svg b/scripts/shapes/assets/shapes-d.svg
new file mode 100644
index 0000000000..fa64b519b9
--- /dev/null
+++ b/scripts/shapes/assets/shapes-d.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/scripts/shapes/assets/shapes-i.svg b/scripts/shapes/assets/shapes-i.svg
new file mode 100644
index 0000000000..cc9df9e64a
--- /dev/null
+++ b/scripts/shapes/assets/shapes-i.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/back-heading.svg b/scripts/shapes/assets/tools/back-heading.svg
new file mode 100644
index 0000000000..d70f315ea1
--- /dev/null
+++ b/scripts/shapes/assets/tools/back-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/back-icon.svg b/scripts/shapes/assets/tools/back-icon.svg
new file mode 100644
index 0000000000..7de1781804
--- /dev/null
+++ b/scripts/shapes/assets/tools/back-icon.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/clone-icon.svg b/scripts/shapes/assets/tools/clone-icon.svg
new file mode 100644
index 0000000000..324c7d57ba
--- /dev/null
+++ b/scripts/shapes/assets/tools/clone-icon.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/clone-label.svg b/scripts/shapes/assets/tools/clone-label.svg
new file mode 100644
index 0000000000..1a141714e8
--- /dev/null
+++ b/scripts/shapes/assets/tools/clone-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/clone-tool-heading.svg b/scripts/shapes/assets/tools/clone-tool-heading.svg
new file mode 100644
index 0000000000..6ab57cd0e1
--- /dev/null
+++ b/scripts/shapes/assets/tools/clone-tool-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/color-icon.svg b/scripts/shapes/assets/tools/color-icon.svg
new file mode 100644
index 0000000000..9363b7607f
--- /dev/null
+++ b/scripts/shapes/assets/tools/color-icon.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/color-label.svg b/scripts/shapes/assets/tools/color-label.svg
new file mode 100644
index 0000000000..008b7b963d
--- /dev/null
+++ b/scripts/shapes/assets/tools/color-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/color-tool-heading.svg b/scripts/shapes/assets/tools/color-tool-heading.svg
new file mode 100644
index 0000000000..5b1979e776
--- /dev/null
+++ b/scripts/shapes/assets/tools/color-tool-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/color/color-circle-black.png b/scripts/shapes/assets/tools/color/color-circle-black.png
new file mode 100644
index 0000000000..4b62c28a4d
Binary files /dev/null and b/scripts/shapes/assets/tools/color/color-circle-black.png differ
diff --git a/scripts/shapes/assets/tools/color/color-circle.png b/scripts/shapes/assets/tools/color/color-circle.png
new file mode 100644
index 0000000000..8bf2613382
Binary files /dev/null and b/scripts/shapes/assets/tools/color/color-circle.png differ
diff --git a/scripts/shapes/assets/tools/color/pick-color-label.svg b/scripts/shapes/assets/tools/color/pick-color-label.svg
new file mode 100644
index 0000000000..6fa2997328
--- /dev/null
+++ b/scripts/shapes/assets/tools/color/pick-color-label.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/color/slider-alpha.png b/scripts/shapes/assets/tools/color/slider-alpha.png
new file mode 100644
index 0000000000..e7693935b2
Binary files /dev/null and b/scripts/shapes/assets/tools/color/slider-alpha.png differ
diff --git a/scripts/shapes/assets/tools/color/slider-white.png b/scripts/shapes/assets/tools/color/slider-white.png
new file mode 100644
index 0000000000..048936b106
Binary files /dev/null and b/scripts/shapes/assets/tools/color/slider-white.png differ
diff --git a/scripts/shapes/assets/tools/color/swatches-label.svg b/scripts/shapes/assets/tools/color/swatches-label.svg
new file mode 100644
index 0000000000..76c9d6228f
--- /dev/null
+++ b/scripts/shapes/assets/tools/color/swatches-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/common/actions-label.svg b/scripts/shapes/assets/tools/common/actions-label.svg
new file mode 100644
index 0000000000..d5428ae3f1
--- /dev/null
+++ b/scripts/shapes/assets/tools/common/actions-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/common/down-arrow.svg b/scripts/shapes/assets/tools/common/down-arrow.svg
new file mode 100644
index 0000000000..352b830aad
--- /dev/null
+++ b/scripts/shapes/assets/tools/common/down-arrow.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/common/finish-label.svg b/scripts/shapes/assets/tools/common/finish-label.svg
new file mode 100644
index 0000000000..58120a337a
--- /dev/null
+++ b/scripts/shapes/assets/tools/common/finish-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/common/info-icon.svg b/scripts/shapes/assets/tools/common/info-icon.svg
new file mode 100644
index 0000000000..ef2495b728
--- /dev/null
+++ b/scripts/shapes/assets/tools/common/info-icon.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/common/up-arrow.svg b/scripts/shapes/assets/tools/common/up-arrow.svg
new file mode 100644
index 0000000000..da80a4e9ce
--- /dev/null
+++ b/scripts/shapes/assets/tools/common/up-arrow.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/delete-icon.svg b/scripts/shapes/assets/tools/delete-icon.svg
new file mode 100644
index 0000000000..f77d40f1e6
--- /dev/null
+++ b/scripts/shapes/assets/tools/delete-icon.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/delete-label.svg b/scripts/shapes/assets/tools/delete-label.svg
new file mode 100644
index 0000000000..e63000e209
--- /dev/null
+++ b/scripts/shapes/assets/tools/delete-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/delete-tool-heading.svg b/scripts/shapes/assets/tools/delete-tool-heading.svg
new file mode 100644
index 0000000000..e92e3c1d00
--- /dev/null
+++ b/scripts/shapes/assets/tools/delete-tool-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/delete/info-text.svg b/scripts/shapes/assets/tools/delete/info-text.svg
new file mode 100644
index 0000000000..4148bd8525
--- /dev/null
+++ b/scripts/shapes/assets/tools/delete/info-text.svg
@@ -0,0 +1,83 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/group-icon.svg b/scripts/shapes/assets/tools/group-icon.svg
new file mode 100644
index 0000000000..56abd0a30c
--- /dev/null
+++ b/scripts/shapes/assets/tools/group-icon.svg
@@ -0,0 +1,33 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/group-label.svg b/scripts/shapes/assets/tools/group-label.svg
new file mode 100644
index 0000000000..001ce5b953
--- /dev/null
+++ b/scripts/shapes/assets/tools/group-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/group-tool-heading.svg b/scripts/shapes/assets/tools/group-tool-heading.svg
new file mode 100644
index 0000000000..e1942213e2
--- /dev/null
+++ b/scripts/shapes/assets/tools/group-tool-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/group/clear-label.svg b/scripts/shapes/assets/tools/group/clear-label.svg
new file mode 100644
index 0000000000..8bc5daa8b4
--- /dev/null
+++ b/scripts/shapes/assets/tools/group/clear-label.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/group/group-label.svg b/scripts/shapes/assets/tools/group/group-label.svg
new file mode 100644
index 0000000000..b2f15b4b22
--- /dev/null
+++ b/scripts/shapes/assets/tools/group/group-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/group/selection-box-label.svg b/scripts/shapes/assets/tools/group/selection-box-label.svg
new file mode 100644
index 0000000000..21bed98a90
--- /dev/null
+++ b/scripts/shapes/assets/tools/group/selection-box-label.svg
@@ -0,0 +1,33 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/group/ungroup-label.svg b/scripts/shapes/assets/tools/group/ungroup-label.svg
new file mode 100644
index 0000000000..ec246359b5
--- /dev/null
+++ b/scripts/shapes/assets/tools/group/ungroup-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics-icon.svg b/scripts/shapes/assets/tools/physics-icon.svg
new file mode 100644
index 0000000000..ef2635c312
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics-icon.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics-label.svg b/scripts/shapes/assets/tools/physics-label.svg
new file mode 100644
index 0000000000..27006f62b8
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics-tool-heading.svg b/scripts/shapes/assets/tools/physics-tool-heading.svg
new file mode 100644
index 0000000000..fb5d696111
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics-tool-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/buttons/collisions-label.svg b/scripts/shapes/assets/tools/physics/buttons/collisions-label.svg
new file mode 100644
index 0000000000..ff3cb0dff3
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/buttons/collisions-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg b/scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg
new file mode 100644
index 0000000000..5b1b797540
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/buttons/grabbable-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/buttons/gravity-label.svg b/scripts/shapes/assets/tools/physics/buttons/gravity-label.svg
new file mode 100644
index 0000000000..6c92a792ef
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/buttons/gravity-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/buttons/off-label.svg b/scripts/shapes/assets/tools/physics/buttons/off-label.svg
new file mode 100644
index 0000000000..0299aa9864
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/buttons/off-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/buttons/on-label.svg b/scripts/shapes/assets/tools/physics/buttons/on-label.svg
new file mode 100644
index 0000000000..a17a5091ad
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/buttons/on-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets-label.svg b/scripts/shapes/assets/tools/physics/presets-label.svg
new file mode 100644
index 0000000000..abe9c5c084
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/balloon-label.svg b/scripts/shapes/assets/tools/physics/presets/balloon-label.svg
new file mode 100644
index 0000000000..920e5f3c51
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/balloon-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/cotton-label.svg b/scripts/shapes/assets/tools/physics/presets/cotton-label.svg
new file mode 100644
index 0000000000..abc77ae8f3
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/cotton-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/custom-label.svg b/scripts/shapes/assets/tools/physics/presets/custom-label.svg
new file mode 100644
index 0000000000..7081e5b35d
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/custom-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/default-label.svg b/scripts/shapes/assets/tools/physics/presets/default-label.svg
new file mode 100644
index 0000000000..24b053750d
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/default-label.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/physics/presets/ice-label.svg b/scripts/shapes/assets/tools/physics/presets/ice-label.svg
new file mode 100644
index 0000000000..b919283e9f
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/ice-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/lead-label.svg b/scripts/shapes/assets/tools/physics/presets/lead-label.svg
new file mode 100644
index 0000000000..a2119fb8f0
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/lead-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/rubber-label.svg b/scripts/shapes/assets/tools/physics/presets/rubber-label.svg
new file mode 100644
index 0000000000..5b4ed55053
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/rubber-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg b/scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg
new file mode 100644
index 0000000000..ecd824257e
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/tumbleweed-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/wood-label.svg b/scripts/shapes/assets/tools/physics/presets/wood-label.svg
new file mode 100644
index 0000000000..59c348fd52
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/wood-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/presets/zero-g-label.svg b/scripts/shapes/assets/tools/physics/presets/zero-g-label.svg
new file mode 100644
index 0000000000..a1a349b8c5
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/presets/zero-g-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/properties-label.svg b/scripts/shapes/assets/tools/physics/properties-label.svg
new file mode 100644
index 0000000000..25198edb62
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/properties-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/sliders/bounce-label.svg b/scripts/shapes/assets/tools/physics/sliders/bounce-label.svg
new file mode 100644
index 0000000000..a997fa80a2
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/sliders/bounce-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/sliders/density-label.svg b/scripts/shapes/assets/tools/physics/sliders/density-label.svg
new file mode 100644
index 0000000000..ec5fd5cf98
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/sliders/density-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/sliders/friction-label.svg b/scripts/shapes/assets/tools/physics/sliders/friction-label.svg
new file mode 100644
index 0000000000..2ff20a71cb
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/sliders/friction-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/physics/sliders/gravity-label.svg b/scripts/shapes/assets/tools/physics/sliders/gravity-label.svg
new file mode 100644
index 0000000000..692a4ebf79
--- /dev/null
+++ b/scripts/shapes/assets/tools/physics/sliders/gravity-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/redo-icon.svg b/scripts/shapes/assets/tools/redo-icon.svg
new file mode 100644
index 0000000000..bd0cea1330
--- /dev/null
+++ b/scripts/shapes/assets/tools/redo-icon.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/redo-label.svg b/scripts/shapes/assets/tools/redo-label.svg
new file mode 100644
index 0000000000..19ae558bb9
--- /dev/null
+++ b/scripts/shapes/assets/tools/redo-label.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/stretch-icon.svg b/scripts/shapes/assets/tools/stretch-icon.svg
new file mode 100644
index 0000000000..dc0547b813
--- /dev/null
+++ b/scripts/shapes/assets/tools/stretch-icon.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/stretch-label.svg b/scripts/shapes/assets/tools/stretch-label.svg
new file mode 100644
index 0000000000..0aecd01a84
--- /dev/null
+++ b/scripts/shapes/assets/tools/stretch-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/stretch-tool-heading.svg b/scripts/shapes/assets/tools/stretch-tool-heading.svg
new file mode 100644
index 0000000000..0d3fde298c
--- /dev/null
+++ b/scripts/shapes/assets/tools/stretch-tool-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/stretch/info-text.svg b/scripts/shapes/assets/tools/stretch/info-text.svg
new file mode 100644
index 0000000000..4bd23f7b7f
--- /dev/null
+++ b/scripts/shapes/assets/tools/stretch/info-text.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/tool-icon.fbx b/scripts/shapes/assets/tools/tool-icon.fbx
new file mode 100644
index 0000000000..b4a21c523b
Binary files /dev/null and b/scripts/shapes/assets/tools/tool-icon.fbx differ
diff --git a/scripts/shapes/assets/tools/tool-label.svg b/scripts/shapes/assets/tools/tool-label.svg
new file mode 100644
index 0000000000..bc8b059bdb
--- /dev/null
+++ b/scripts/shapes/assets/tools/tool-label.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/tools-heading.svg b/scripts/shapes/assets/tools/tools-heading.svg
new file mode 100644
index 0000000000..e180ae7251
--- /dev/null
+++ b/scripts/shapes/assets/tools/tools-heading.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/scripts/shapes/assets/tools/undo-icon.svg b/scripts/shapes/assets/tools/undo-icon.svg
new file mode 100644
index 0000000000..566de28906
--- /dev/null
+++ b/scripts/shapes/assets/tools/undo-icon.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/scripts/shapes/assets/tools/undo-label.svg b/scripts/shapes/assets/tools/undo-label.svg
new file mode 100644
index 0000000000..ca749f2765
--- /dev/null
+++ b/scripts/shapes/assets/tools/undo-label.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/scripts/shapes/modules/createPalette.js b/scripts/shapes/modules/createPalette.js
new file mode 100644
index 0000000000..0eea8379d6
--- /dev/null
+++ b/scripts/shapes/modules/createPalette.js
@@ -0,0 +1,546 @@
+//
+// createPalette.js
+//
+// Created by David Rowe on 28 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global CreatePalette: true, App, Feedback, History, UIT */
+
+CreatePalette = function (side, leftInputs, rightInputs, uiCommandCallback) {
+ // Tool menu displayed on top of forearm.
+
+ "use strict";
+
+ var paletteOriginOverlay,
+ paletteHeaderHeadingOverlay,
+ paletteHeaderBarOverlay,
+ paletteTitleOverlay,
+ palettePanelOverlay,
+ paletteItemOverlays = [],
+ paletteItemPositions = [],
+ paletteItemHoverOverlays = [],
+ iconOverlays = [],
+ staticOverlays = [],
+
+ LEFT_HAND = 0,
+
+ controlJointName,
+
+ PALETTE_ORIGIN_POSITION = {
+ x: 0,
+ y: UIT.dimensions.handOffset,
+ z: UIT.dimensions.canvasSeparation + UIT.dimensions.canvas.x / 2
+ },
+ PALETTE_ORIGIN_ROTATION = Quat.ZERO,
+ paletteLateralOffset,
+
+ PALETTE_ORIGIN_PROPERTIES = {
+ dimensions: { x: 0.005, y: 0.005, z: 0.005 },
+ localPosition: PALETTE_ORIGIN_POSITION,
+ localRotation: PALETTE_ORIGIN_ROTATION,
+ color: { red: 255, blue: 0, green: 0 },
+ alpha: 1.0,
+ parentID: Uuid.SELF,
+ ignoreRayIntersection: true,
+ visible: false
+ },
+
+ PALETTE_HEADER_HEADING_PROPERTIES = {
+ url: Script.resolvePath("../assets/gray-header.fbx"),
+ dimensions: UIT.dimensions.headerHeading, // Model is in rotated coordinate system but can override.
+ localPosition: {
+ x: 0,
+ y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y / 2,
+ z: UIT.dimensions.headerHeading.z / 2
+ },
+ localRotation: Quat.ZERO,
+ alpha: 1.0,
+ solid: true,
+ ignoreRayIntersection: false,
+ visible: true
+ },
+
+ PALETTE_HEADER_BAR_PROPERTIES = {
+ url: Script.resolvePath("../assets/blue-header-bar.fbx"),
+ dimensions: UIT.dimensions.headerBar, // Model is in rotated coordinate system but can override.
+ localPosition: {
+ x: 0,
+ y: UIT.dimensions.canvas.y / 2 - UIT.dimensions.headerHeading.y - UIT.dimensions.headerBar.y / 2,
+ z: UIT.dimensions.headerBar.z / 2
+ },
+ localRotation: Quat.ZERO,
+ alpha: 1.0,
+ solid: true,
+ ignoreRayIntersection: false,
+ visible: true
+ },
+
+ PALETTE_TITLE_PROPERTIES = {
+ url: Script.resolvePath("../assets/create/create-heading.svg"),
+ scale: 0.0363,
+ localPosition: {
+ x: 0,
+ y: 0,
+ z: PALETTE_HEADER_HEADING_PROPERTIES.dimensions.z / 2 + UIT.dimensions.imageOverlayOffset
+ },
+ localRotation: Quat.ZERO,
+ color: UIT.colors.white,
+ alpha: 1.0,
+ emissive: true,
+ ignoreRayIntersection: true,
+ isFacingAvatar: false,
+ visible: true
+ },
+
+ PALETTE_PANEL_PROPERTIES = {
+ dimensions: UIT.dimensions.panel,
+ localPosition: { x: 0, y: (UIT.dimensions.panel.y - UIT.dimensions.canvas.y) / 2, z: UIT.dimensions.panel.z / 2 },
+ localRotation: Quat.ZERO,
+ color: UIT.colors.baseGray,
+ alpha: 1.0,
+ solid: true,
+ ignoreRayIntersection: false,
+ visible: true
+ },
+
+ ENTITY_CREATION_DIMENSIONS = { x: 0.2, y: 0.2, z: 0.2 },
+ ENTITY_CREATION_COLOR = { red: 192, green: 192, blue: 192 },
+
+ PALETTE_ITEM = {
+ overlay: "cube", // Invisible cube for hit area.
+ properties: {
+ dimensions: UIT.dimensions.itemCollisionZone,
+ localRotation: Quat.ZERO,
+ alpha: 0.0, // Invisible.
+ solid: true,
+ ignoreRayIntersection: false,
+ visible: true // So that laser intersects.
+ },
+ hoverButton: {
+ // Relative to root overlay.
+ overlay: "cube",
+ properties: {
+ dimensions: UIT.dimensions.paletteItemButtonDimensions,
+ localPosition: UIT.dimensions.paletteItemButtonOffset,
+ localRotation: Quat.ZERO,
+ color: UIT.colors.blueHighlight,
+ alpha: 1.0,
+ emissive: true, // TODO: This has no effect.
+ solid: true,
+ ignoreRayIntersection: true,
+ visible: false
+ }
+ },
+ icon: {
+ // Relative to hoverButton.
+ overlay: "model",
+ properties: {
+ dimensions: UIT.dimensions.paletteItemIconDimensions,
+ localPosition: UIT.dimensions.paletteItemIconOffset,
+ localRotation: Quat.ZERO,
+ emissive: true, // TODO: This has no effect.
+ ignoreRayIntersection: true
+ }
+ },
+ entity: {
+ dimensions: ENTITY_CREATION_DIMENSIONS
+ }
+ },
+
+ PALETTE_ITEMS = [
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/cube.fbx")
+ }
+ },
+ entity: {
+ type: "Box",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/sphere.fbx")
+ }
+ },
+ entity: {
+ type: "Sphere",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/tetrahedron.fbx")
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Tetrahedron",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/octahedron.fbx")
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Octahedron",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/icosahedron.fbx")
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Icosahedron",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/dodecahedron.fbx")
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Dodecahedron",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/hexagon.fbx"),
+ dimensions: { x: 0.02078, y: 0.024, z: 0.024 },
+ localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Hexagon",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/prism.fbx"),
+ localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Triangle",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/octagon.fbx"),
+ dimensions: { x: 0.023805, y: 0.024, z: 0.024 },
+ localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Octagon",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/cylinder.fbx"),
+ localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Cylinder",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/cone.fbx"),
+ localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Cone",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ },
+ {
+ icon: {
+ properties: {
+ url: Script.resolvePath("../assets/create/circle.fbx"),
+ dimensions: { x: 0.024, y: 0.0005, z: 0.024 },
+ localRotation: Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ }
+ },
+ entity: {
+ type: "Shape",
+ shape: "Circle",
+ dimensions: ENTITY_CREATION_DIMENSIONS,
+ color: ENTITY_CREATION_COLOR
+ }
+ }
+ ],
+
+ isDisplaying = false,
+
+ NONE = -1,
+ highlightedItem = NONE,
+ wasTriggerClicked = false,
+ otherSide,
+
+ // References.
+ controlHand;
+
+ if (!(this instanceof CreatePalette)) {
+ return new CreatePalette();
+ }
+
+
+ function setHand(hand) {
+ // Assumes UI is not displaying.
+ var NUMBER_OF_HANDS = 2;
+ side = hand;
+ otherSide = (side + 1) % NUMBER_OF_HANDS;
+ controlHand = side === LEFT_HAND ? rightInputs.hand() : leftInputs.hand();
+ controlJointName = side === LEFT_HAND ? "LeftHand" : "RightHand";
+ paletteLateralOffset = side === LEFT_HAND ? -UIT.dimensions.handLateralOffset : UIT.dimensions.handLateralOffset;
+ }
+
+ setHand(side);
+
+ function getOverlayIDs() {
+ return [palettePanelOverlay, paletteHeaderHeadingOverlay, paletteHeaderBarOverlay].concat(paletteItemOverlays);
+ }
+
+ function setVisible(visible) {
+ var i,
+ length;
+
+ for (i = 0, length = staticOverlays.length; i < length; i++) {
+ Overlays.editOverlay(staticOverlays[i], { visible: visible });
+ }
+
+ if (!visible) {
+ for (i = 0, length = paletteItemHoverOverlays.length; i < length; i++) {
+ Overlays.editOverlay(paletteItemHoverOverlays[i], { visible: false });
+ }
+ }
+ }
+
+ function update(intersectionOverlayID) {
+ var itemIndex,
+ isTriggerClicked,
+ properties,
+ CREATE_OFFSET = { x: 0, y: 0.05, z: -0.02 },
+ INVERSE_HAND_BASIS_ROTATION = Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }),
+ entityID;
+
+ itemIndex = paletteItemOverlays.indexOf(intersectionOverlayID);
+
+ // Unhighlight and lower old item.
+ if (highlightedItem !== NONE && (itemIndex === NONE || itemIndex !== highlightedItem)) {
+ Overlays.editOverlay(paletteItemHoverOverlays[highlightedItem], {
+ localPosition: UIT.dimensions.paletteItemButtonOffset,
+ visible: false
+ });
+ highlightedItem = NONE;
+ }
+
+ // Highlight and raise new item.
+ if (itemIndex !== NONE && highlightedItem !== itemIndex) {
+ Feedback.play(otherSide, Feedback.HOVER_BUTTON);
+ Overlays.editOverlay(paletteItemHoverOverlays[itemIndex], {
+ localPosition: UIT.dimensions.paletteItemButtonHoveredOffset,
+ visible: true
+ });
+ highlightedItem = itemIndex;
+ }
+
+ // Press item and create new entity.
+ isTriggerClicked = controlHand.triggerClicked();
+ if (highlightedItem !== NONE && isTriggerClicked && !wasTriggerClicked) {
+ // Create entity.
+ Feedback.play(otherSide, Feedback.CREATE_ENTITY);
+ properties = Object.clone(PALETTE_ITEMS[itemIndex].entity);
+ properties.position = Vec3.sum(controlHand.palmPosition(),
+ Vec3.multiplyQbyV(controlHand.orientation(),
+ Vec3.sum({ x: 0, y: properties.dimensions.z / 2, z: 0 }, CREATE_OFFSET)));
+ properties.rotation = Quat.multiply(controlHand.orientation(), INVERSE_HAND_BASIS_ROTATION);
+ entityID = Entities.addEntity(properties);
+ if (entityID !== Uuid.NULL) {
+ History.prePush(
+ otherSide,
+ { deleteEntities: [{ entityID: entityID }] },
+ { createEntities: [{ entityID: entityID, properties: properties }] }
+ );
+ } else {
+ Feedback.play(otherSide, Feedback.GENERAL_ERROR);
+ }
+
+ // Lower and unhighlight item.
+ Overlays.editOverlay(paletteItemHoverOverlays[itemIndex], {
+ localPosition: UIT.dimensions.paletteItemButtonOffset,
+ visible: false
+ });
+
+ uiCommandCallback("autoGrab");
+ }
+
+ wasTriggerClicked = isTriggerClicked;
+ }
+
+ function itemPosition(index) {
+ // Position relative to palette panel.
+ var ITEMS_PER_ROW = 4,
+ ROW_ZERO_Y_OFFSET = 0.0860,
+ ROW_SPACING = 0.0560,
+ COLUMN_ZERO_OFFSET = -0.08415,
+ COLUMN_SPACING = 0.0561,
+ row,
+ column;
+
+ row = Math.floor(index / ITEMS_PER_ROW);
+ column = index % ITEMS_PER_ROW;
+
+ return {
+ x: COLUMN_ZERO_OFFSET + column * COLUMN_SPACING,
+ y: ROW_ZERO_Y_OFFSET - row * ROW_SPACING,
+ z: UIT.dimensions.panel.z / 2 + UIT.dimensions.itemCollisionZone.z / 2
+ };
+ }
+
+ function display() {
+ // Creates and shows menu entities.
+ var handJointIndex,
+ properties,
+ i,
+ length;
+
+ if (isDisplaying) {
+ return;
+ }
+
+ // Joint index.
+ handJointIndex = MyAvatar.getJointIndex(controlJointName);
+ if (handJointIndex === NONE) {
+ // Don't display if joint isn't available (yet) to attach to.
+ // User can clear this condition by toggling the app off and back on once avatar finishes loading.
+ App.log(side, "ERROR: CreatePalette: Hand joint index isn't available!");
+ return;
+ }
+
+ // Calculate position to put palette.
+ properties = Object.clone(PALETTE_ORIGIN_PROPERTIES);
+ properties.parentJointIndex = handJointIndex;
+ properties.localPosition = Vec3.sum(PALETTE_ORIGIN_POSITION, { x: paletteLateralOffset, y: 0, z: 0 });
+ paletteOriginOverlay = Overlays.addOverlay("sphere", properties);
+
+ // Header.
+ properties = Object.clone(PALETTE_HEADER_HEADING_PROPERTIES);
+ properties.parentID = paletteOriginOverlay;
+ paletteHeaderHeadingOverlay = Overlays.addOverlay("model", properties);
+ properties = Object.clone(PALETTE_HEADER_BAR_PROPERTIES);
+ properties.parentID = paletteOriginOverlay;
+ paletteHeaderBarOverlay = Overlays.addOverlay("model", properties);
+ properties = Object.clone(PALETTE_TITLE_PROPERTIES);
+ properties.parentID = paletteHeaderHeadingOverlay;
+ paletteTitleOverlay = Overlays.addOverlay("image3d", properties);
+
+ // Palette background.
+ properties = Object.clone(PALETTE_PANEL_PROPERTIES);
+ properties.parentID = paletteOriginOverlay;
+ palettePanelOverlay = Overlays.addOverlay("cube", properties);
+
+ // Palette items.
+ for (i = 0, length = PALETTE_ITEMS.length; i < length; i++) {
+ // Collision overlay.
+ properties = Object.clone(PALETTE_ITEM.properties);
+ properties.parentID = palettePanelOverlay;
+ properties.localPosition = itemPosition(i);
+ paletteItemOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.overlay, properties);
+ paletteItemPositions[i] = properties.localPosition;
+
+ // Highlight overlay.
+ properties = Object.clone(PALETTE_ITEM.hoverButton.properties);
+ properties.parentID = paletteItemOverlays[i];
+ paletteItemHoverOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.hoverButton.overlay, properties);
+
+ // Icon overlay.
+ properties = Object.clone(PALETTE_ITEM.icon.properties);
+ properties = Object.merge(properties, PALETTE_ITEMS[i].icon.properties);
+ properties.parentID = paletteItemHoverOverlays[i];
+ iconOverlays[i] = Overlays.addOverlay(PALETTE_ITEM.icon.overlay, properties);
+ }
+
+ // Always-visible overlays.
+ staticOverlays = [].concat(paletteHeaderHeadingOverlay, paletteHeaderBarOverlay, paletteTitleOverlay,
+ palettePanelOverlay, paletteItemOverlays, iconOverlays);
+
+ isDisplaying = true;
+ }
+
+ function clear() {
+ // Deletes menu entities.
+ if (!isDisplaying) {
+ return;
+ }
+ Overlays.deleteOverlay(paletteOriginOverlay); // Automatically deletes all other overlays because they're children.
+ paletteItemOverlays = [];
+ paletteItemHoverOverlays = [];
+ iconOverlays = [];
+ staticOverlays = [];
+ isDisplaying = false;
+ }
+
+ function destroy() {
+ clear();
+ }
+
+ return {
+ setHand: setHand,
+ overlayIDs: getOverlayIDs,
+ setVisible: setVisible,
+ update: update,
+ display: display,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+CreatePalette.prototype = {};
diff --git a/scripts/shapes/modules/feedback.js b/scripts/shapes/modules/feedback.js
new file mode 100644
index 0000000000..0d45ae2019
--- /dev/null
+++ b/scripts/shapes/modules/feedback.js
@@ -0,0 +1,80 @@
+//
+// feedback.js
+//
+// Created by David Rowe on 31 Aug 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Feedback:true */
+
+Feedback = (function () {
+ // Provide audio and haptic user feedback.
+ // Global object.
+
+ "use strict";
+
+ var DROP_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/drop.wav")),
+ DELETE_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/delete.wav")),
+ SELECT_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/select.wav")),
+ CLONE_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/clone.wav")),
+ CREATE_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/create.wav")),
+ EQUIP_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/equip.wav")),
+ ERROR_SOUND = SoundCache.getSound(Script.resolvePath("../assets/audio/error.wav")),
+ UNDO_SOUND = DROP_SOUND,
+ REDO_SOUND = DROP_SOUND,
+
+ FEEDBACK_PARAMETERS = {
+ DROP_TOOL: { sound: DROP_SOUND, volume: 0.3, haptic: 0.75 },
+ DELETE_ENTITY: { sound: DELETE_SOUND, volume: 0.5, haptic: 0.2 },
+ SELECT_ENTITY: { sound: SELECT_SOUND, volume: 0.2, haptic: 0.1 }, // E.g., Group tool.
+ CLONE_ENTITY: { sound: CLONE_SOUND, volume: 0.2, haptic: 0.1 },
+ CREATE_ENTITY: { sound: CREATE_SOUND, volume: 0.4, haptic: 0.2 },
+ HOVER_MENU_ITEM: { sound: null, volume: 0, haptic: 0.1 }, // Tools menu.
+ HOVER_BUTTON: { sound: null, volume: 0, haptic: 0.075 }, // Tools options and Create palette items.
+ EQUIP_TOOL: { sound: EQUIP_SOUND, volume: 0.3, haptic: 0.6 },
+ APPLY_PROPERTY: { sound: null, volume: 0, haptic: 0.3 },
+ APPLY_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 },
+ UNDO_ACTION: { sound: UNDO_SOUND, volume: 0.1, haptic: 0.2 },
+ REDO_ACTION: { sound: REDO_SOUND, volume: 0.1, haptic: 0.2 },
+ GENERAL_ERROR: { sound: ERROR_SOUND, volume: 0.2, haptic: 0.7 }
+ },
+
+ VOLUME_MULTPLIER = 0.5, // Resulting volume range should be within 0.0 - 1.0.
+ HAPTIC_STRENGTH_MULTIPLIER = 1.3, // Resulting strength range should be within 0.0 - 1.0.
+ HAPTIC_LENGTH_MULTIPLIER = 75.0; // Resulting length range should be within 0 - 50, say.
+
+ function play(side, item) {
+ var parameters = FEEDBACK_PARAMETERS[item];
+
+ if (parameters.sound) {
+ Audio.playSound(parameters.sound, {
+ position: side ? MyAvatar.getRightPalmPosition() : MyAvatar.getLeftPalmPosition(),
+ volume: parameters.volume * VOLUME_MULTPLIER,
+ localOnly: true
+ });
+ }
+
+ Controller.triggerHapticPulse(parameters.haptic * HAPTIC_STRENGTH_MULTIPLIER,
+ parameters.haptic * HAPTIC_LENGTH_MULTIPLIER, side);
+ }
+
+ return {
+ DROP_TOOL: "DROP_TOOL",
+ DELETE_ENTITY: "DELETE_ENTITY",
+ SELECT_ENTITY: "SELECT_ENTITY",
+ CLONE_ENTITY: "CLONE_ENTITY",
+ CREATE_ENTITY: "CREATE_ENTITY",
+ HOVER_MENU_ITEM: "HOVER_MENU_ITEM",
+ HOVER_BUTTON: "HOVER_BUTTON",
+ EQUIP_TOOL: "EQUIP_TOOL",
+ APPLY_PROPERTY: "APPLY_PROPERTY",
+ APPLY_ERROR: "APPLY_ERROR",
+ UNDO_ACTION: "UNDO_ACTION",
+ REDO_ACTION: "REDO_ACTION",
+ GENERAL_ERROR: "GENERAL_ERROR",
+ play: play
+ };
+}());
diff --git a/scripts/shapes/modules/groups.js b/scripts/shapes/modules/groups.js
new file mode 100644
index 0000000000..3153a622ee
--- /dev/null
+++ b/scripts/shapes/modules/groups.js
@@ -0,0 +1,275 @@
+//
+// groups.js
+//
+// Created by David Rowe on 1 Aug 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Groups:true, App, History */
+
+Groups = function () {
+ // Groups and ungroups trees of entities.
+
+ "use strict";
+
+ var rootEntityIDs = [],
+ selections = [],
+ entitiesSelectedCount = 0;
+
+ if (!(this instanceof Groups)) {
+ return new Groups();
+ }
+
+ function add(selection) {
+ rootEntityIDs.push(selection[0].id);
+ selections.push(Object.clone(selection));
+ entitiesSelectedCount += selection.length;
+ }
+
+ function remove(selection) {
+ var index = rootEntityIDs.indexOf(selection[0].id);
+
+ entitiesSelectedCount -= selections[index].length;
+ rootEntityIDs.splice(index, 1);
+ selections.splice(index, 1);
+ }
+
+ function toggle(selection) {
+ if (selection.length === 0) {
+ return;
+ }
+
+ if (rootEntityIDs.indexOf(selection[0].id) === -1) {
+ add(selection);
+ } else {
+ remove(selection);
+ }
+ }
+
+ function selection(excludes) {
+ var result = [],
+ i,
+ lengthI,
+ j,
+ lengthJ;
+
+ for (i = 0, lengthI = rootEntityIDs.length; i < lengthI; i++) {
+ if (excludes.indexOf(rootEntityIDs[i]) === -1) {
+ for (j = 0, lengthJ = selections[i].length; j < lengthJ; j++) {
+ result.push(selections[i][j]);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ function includes(rootEntityID) {
+ return rootEntityIDs.indexOf(rootEntityID) !== -1;
+ }
+
+ function getRootEntityIDs() {
+ return rootEntityIDs;
+ }
+
+ function groupsCount() {
+ return selections.length;
+ }
+
+ function entitiesCount() {
+ return entitiesSelectedCount;
+ }
+
+ function group() {
+ // Groups all selections into one.
+ var DYNAMIC_AND_COLLISIONLESS = { dynamic: true, collisionless: true },
+ rootID,
+ undoData = [],
+ redoData = [],
+ i,
+ lengthI,
+ j,
+ lengthJ;
+
+ // If the first group has physics (i.e., root entity is dynamic) make all entities in child groups dynamic and
+ // collisionless. (Don't need to worry about other groups physics properties because only those of the first entity in
+ // the linkset are used by High Fidelity.) See Selection.applyPhysics().
+ if (selections[0][0].dynamic) {
+ for (i = 1, lengthI = selections.length; i < lengthI; i++) {
+ for (j = 0, lengthJ = selections[i].length; j < lengthJ; j++) {
+ undoData.push({
+ entityID: selections[i][j].id,
+ properties: {
+ dynamic: selections[i][j].dynamic,
+ collisionless: selections[i][j].collisionless
+ }
+ });
+ Entities.editEntity(selections[i][j].id, DYNAMIC_AND_COLLISIONLESS);
+ selections[i][j].dynamic = true;
+ selections[i][j].collisionless = true;
+ redoData.push({
+ entityID: selections[i][j].id,
+ properties: DYNAMIC_AND_COLLISIONLESS
+ });
+ }
+ }
+ }
+
+ // Make the first entity in the first group the root and link the first entities of all other groups to it.
+ rootID = rootEntityIDs[0];
+ for (i = 1, lengthI = rootEntityIDs.length; i < lengthI; i++) {
+ undoData.push({
+ entityID: rootEntityIDs[i],
+ properties: { parentID: Uuid.NULL }
+ });
+ Entities.editEntity(rootEntityIDs[i], {
+ parentID: rootID
+ });
+ redoData.push({
+ entityID: rootEntityIDs[i],
+ properties: { parentID: rootID }
+ });
+ }
+
+ // Update selection.
+ rootEntityIDs.splice(1, rootEntityIDs.length - 1);
+ for (i = 1, lengthI = selections.length; i < lengthI; i++) {
+ selections[i][0].parentID = rootID;
+ selections[0] = selections[0].concat(selections[i]);
+ }
+ selections.splice(1, selections.length - 1);
+
+ // Add history entry.
+ History.push(null, { setProperties: undoData }, { setProperties: redoData });
+ }
+
+ function ungroup() {
+ // Ungroups the first and assumed to be only selection.
+ // If the first entity in the selection has a mix of solo and group children then just the group children are unlinked,
+ // otherwise all are unlinked.
+ var rootID,
+ childrenIDs = [],
+ childrenIndexes = [],
+ childrenIndexIsGroup = [],
+ previousWasGroup,
+ hasSoloChildren = false,
+ hasGroupChildren = false,
+ isUngroupAll,
+ NONDYNAMIC_AND_NONCOLLISIONLESS = { dynamic: false, collisionless: false },
+ undoData = [],
+ redoData = [],
+ i,
+ lengthI,
+ j,
+ lengthJ;
+
+ function updateGroupInformation() {
+ var childrenIndexesLength = childrenIndexes.length;
+ if (childrenIndexesLength > 1) {
+ previousWasGroup = childrenIndexes[childrenIndexesLength - 2] < i - 1;
+ childrenIndexIsGroup.push(previousWasGroup);
+ if (previousWasGroup) {
+ hasGroupChildren = true;
+ } else {
+ hasSoloChildren = true;
+ }
+ }
+ }
+
+ if (entitiesSelectedCount === 0) {
+ App.log("ERROR: Groups: Nothing to ungroup!");
+ return;
+ }
+ if (entitiesSelectedCount === 1) {
+ App.log("ERROR: Groups: Cannot ungroup sole entity!");
+ return;
+ }
+
+ // Compile information on immediate children.
+ rootID = rootEntityIDs[0];
+ for (i = 1, lengthI = selections[0].length; i < lengthI; i++) {
+ if (selections[0][i].parentID === rootID) {
+ childrenIDs.push(selections[0][i].id);
+ childrenIndexes.push(i);
+ updateGroupInformation();
+ }
+ }
+ childrenIndexes.push(selections[0].length); // Special extra item at end to aid updating selection.
+ updateGroupInformation();
+
+ // Unlink children.
+ isUngroupAll = hasSoloChildren !== hasGroupChildren;
+ for (i = childrenIDs.length - 1; i >= 0; i--) {
+ if (isUngroupAll || childrenIndexIsGroup[i]) {
+ undoData.push({
+ entityID: childrenIDs[i],
+ properties: { parentID: selections[0][childrenIndexes[i]].parentID }
+ });
+ Entities.editEntity(childrenIDs[i], {
+ parentID: Uuid.NULL
+ });
+ redoData.push({
+ entityID: childrenIDs[i],
+ properties: { parentID: Uuid.NULL }
+ });
+ rootEntityIDs.push(childrenIDs[i]);
+ selections[0][childrenIndexes[i]].parentID = Uuid.NULL;
+ selections.push(selections[0].splice(childrenIndexes[i], childrenIndexes[i + 1] - childrenIndexes[i]));
+ }
+ }
+
+ // If root group has physics, reset child groups to defaults for dynamic and collisionless. See
+ // Selection.applyPhysics().
+ if (selections[0][0].dynamic) {
+ for (i = 1, lengthI = selections.length; i < lengthI; i++) {
+ for (j = 0, lengthJ = selections[i].length; j < lengthJ; j++) {
+ undoData.push({
+ entityID: selections[i][j].id,
+ properties: {
+ dynamic: selections[i][j].dynamic,
+ collisionless: selections[i][j].collisionless
+ }
+ });
+ Entities.editEntity(selections[i][j].id, NONDYNAMIC_AND_NONCOLLISIONLESS);
+ selections[i][j].dynamic = false;
+ selections[i][j].collisionless = false;
+ redoData.push({
+ entityID: selections[i][j].id,
+ properties: NONDYNAMIC_AND_NONCOLLISIONLESS
+ });
+ }
+ }
+ }
+
+ // Add history entry.
+ History.push(null, { setProperties: undoData }, { setProperties: redoData });
+ }
+
+ function clear() {
+ rootEntityIDs = [];
+ selections = [];
+ entitiesSelectedCount = 0;
+ }
+
+ function destroy() {
+ clear();
+ }
+
+ return {
+ toggle: toggle,
+ selection: selection,
+ includes: includes,
+ rootEntityIDs: getRootEntityIDs,
+ groupsCount: groupsCount,
+ entitiesCount: entitiesCount,
+ group: group,
+ ungroup: ungroup,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+Groups.prototype = {};
diff --git a/scripts/shapes/modules/hand.js b/scripts/shapes/modules/hand.js
new file mode 100644
index 0000000000..73efed8017
--- /dev/null
+++ b/scripts/shapes/modules/hand.js
@@ -0,0 +1,250 @@
+//
+// hand.js
+//
+// Created by David Rowe on 21 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Hand:true */
+
+Hand = function (side) {
+
+ "use strict";
+
+ // Hand controller input.
+ var handController,
+ controllerTrigger,
+ controllerTriggerClicked,
+ controllerGrip,
+
+ isGripClicked = false,
+ isGripClickedHandled = false,
+ GRIP_ON_VALUE = 0.99,
+ GRIP_OFF_VALUE = 0.95,
+
+ isTriggerPressed,
+ isTriggerClicked,
+ TRIGGER_ON_VALUE = 0.15, // Per controllerDispatcherUtils.js.
+ TRIGGER_OFF_VALUE = 0.1, // Per controllerDispatcherUtils.js.
+ TRIGGER_CLICKED_VALUE = 1.0,
+
+ NEAR_GRAB_RADIUS = 0.05, // Different from controllerDispatcherUtils.js.
+ NEAR_HOVER_RADIUS = 0.025,
+
+ LEFT_HAND = 0,
+ HALF_TREE_SCALE = 16384,
+
+ handPose,
+ handPosition,
+ handOrientation,
+ palmPosition,
+
+ handleOverlayIDs = [],
+ intersection = {};
+
+ if (!(this instanceof Hand)) {
+ return new Hand(side);
+ }
+
+ if (side === LEFT_HAND) {
+ handController = Controller.Standard.LeftHand;
+ controllerTrigger = Controller.Standard.LT;
+ controllerTriggerClicked = Controller.Standard.LTClick;
+ controllerGrip = Controller.Standard.LeftGrip;
+ } else {
+ handController = Controller.Standard.RightHand;
+ controllerTrigger = Controller.Standard.RT;
+ controllerTriggerClicked = Controller.Standard.RTClick;
+ controllerGrip = Controller.Standard.RightGrip;
+ }
+
+ function setHandleOverlays(overlayIDs) {
+ handleOverlayIDs = overlayIDs;
+ }
+
+ function valid() {
+ return handPose.valid;
+ }
+
+ function position() {
+ return handPosition;
+ }
+
+ function orientation() {
+ return handOrientation;
+ }
+
+ function getPalmPosition() {
+ return palmPosition;
+ }
+
+ function triggerPressed() {
+ return isTriggerPressed;
+ }
+
+ function triggerClicked() {
+ return isTriggerClicked;
+ }
+
+ function gripClicked() {
+ return isGripClicked;
+ }
+
+ function setGripClickedHandled() {
+ isGripClicked = false;
+ isGripClickedHandled = true;
+ }
+
+ function getIntersection() {
+ return intersection;
+ }
+
+ function getNearGrabRadius() {
+ return NEAR_GRAB_RADIUS;
+ }
+
+ function update() {
+ var gripValue,
+ overlayID,
+ overlayIDs,
+ overlayDistance,
+ intersectionPosition,
+ distance,
+ entityID,
+ entityIDs,
+ entitySize,
+ size,
+ i,
+ length;
+
+
+ // Hand pose.
+ handPose = Controller.getPoseValue(handController);
+ if (!handPose.valid) {
+ intersection = {};
+ return;
+ }
+ handPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, handPose.translation), MyAvatar.position);
+ handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation);
+
+ // Controller trigger.
+ isTriggerPressed = Controller.getValue(controllerTrigger) > (isTriggerPressed
+ ? TRIGGER_OFF_VALUE : TRIGGER_ON_VALUE);
+ isTriggerClicked = Controller.getValue(controllerTriggerClicked) === TRIGGER_CLICKED_VALUE;
+
+ // Controller grip.
+ gripValue = Controller.getValue(controllerGrip);
+ if (isGripClicked) {
+ isGripClicked = gripValue > GRIP_OFF_VALUE;
+ } else {
+ isGripClicked = gripValue > GRIP_ON_VALUE;
+ }
+ // Grip clicked may be being handled by UI.
+ if (isGripClicked) {
+ isGripClicked = !isGripClickedHandled;
+ } else {
+ isGripClickedHandled = false;
+ }
+
+ // Hand-overlay intersection, if any handle overlays.
+ overlayID = null;
+ palmPosition = side === LEFT_HAND ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition();
+ if (handleOverlayIDs.length > 0) {
+ overlayIDs = Overlays.findOverlays(palmPosition, NEAR_HOVER_RADIUS);
+ if (overlayIDs.length > 0) {
+ // Typically, there will be only one overlay; optimize for that case.
+ overlayID = overlayIDs[0];
+ if (overlayIDs.length > 1) {
+ // Find closest overlay.
+ overlayDistance = Vec3.length(Vec3.subtract(Overlays.getProperty(overlayID, "position"), palmPosition));
+ for (i = 1, length = overlayIDs.length; i < length; i++) {
+ distance =
+ Vec3.length(Vec3.subtract(Overlays.getProperty(overlayIDs[i], "position"), palmPosition));
+ if (distance > overlayDistance) {
+ overlayID = overlayIDs[i];
+ overlayDistance = distance;
+ }
+ }
+ }
+ }
+ if (overlayID && handleOverlayIDs.indexOf(overlayID) === -1) {
+ overlayID = null;
+ }
+ if (overlayID) {
+ intersectionPosition = Overlays.getProperty(overlayID, "position");
+ }
+ }
+
+ // Hand-entity intersection, if any editable, if overlay not intersected.
+ entityID = null;
+ if (overlayID === null) {
+ // palmPosition is set above.
+ entityIDs = Entities.findEntities(palmPosition, NEAR_GRAB_RADIUS);
+ if (entityIDs.length > 0) {
+ // Typically, there will be only one entity; optimize for that case.
+ if (Entities.hasEditableRoot(entityIDs[0])) {
+ entityID = entityIDs[0];
+ }
+ if (entityIDs.length > 1) {
+ // Find smallest, editable entity.
+ entitySize = HALF_TREE_SCALE;
+ for (i = 0, length = entityIDs.length; i < length; i++) {
+ if (Entities.hasEditableRoot(entityIDs[i])) {
+ size = Vec3.length(Entities.getEntityProperties(entityIDs[i], "dimensions").dimensions);
+ if (size < entitySize) {
+ entityID = entityIDs[i];
+ entitySize = size;
+ }
+ }
+ }
+ }
+ }
+ if (entityID) {
+ intersectionPosition = Entities.getEntityProperties(entityID, "position").position;
+ if (Vec3.distance(palmPosition, intersectionPosition) > NEAR_GRAB_RADIUS) {
+ intersectionPosition = Vec3.sum(palmPosition,
+ Vec3.multiply(NEAR_GRAB_RADIUS, Vec3.normalize(Vec3.subtract(intersectionPosition, palmPosition))));
+ }
+ }
+ }
+
+ intersection = {
+ intersects: overlayID !== null || entityID !== null,
+ overlayID: overlayID,
+ entityID: entityID,
+ handIntersected: overlayID !== null || entityID !== null,
+ editableEntity: entityID !== null,
+ intersection: intersectionPosition
+ };
+ }
+
+ function clear() {
+ // Nothing to do.
+ }
+
+ function destroy() {
+ // Nothing to do.
+ }
+
+ return {
+ setHandleOverlays: setHandleOverlays,
+ valid: valid,
+ position: position,
+ orientation: orientation,
+ palmPosition: getPalmPosition,
+ triggerPressed: triggerPressed,
+ triggerClicked: triggerClicked,
+ gripClicked: gripClicked,
+ setGripClickedHandled: setGripClickedHandled,
+ intersection: getIntersection,
+ getNearGrabRadius: getNearGrabRadius,
+ update: update,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+Hand.prototype = {};
diff --git a/scripts/shapes/modules/handles.js b/scripts/shapes/modules/handles.js
new file mode 100644
index 0000000000..d7627e8626
--- /dev/null
+++ b/scripts/shapes/modules/handles.js
@@ -0,0 +1,372 @@
+//
+// handles.js
+//
+// Created by David Rowe on 21 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Handles:true */
+
+Handles = function (side) {
+ // Draws scaling handles.
+
+ "use strict";
+
+ var boundingBoxOverlay,
+ boundingBoxDimensions,
+ boundingBoxLocalCenter,
+ cornerIndexes = [],
+ cornerHandleOverlays = [],
+ faceHandleOverlays = [],
+ faceHandleOffsets,
+ BOUNDING_BOX_COLOR = { red: 0, green: 240, blue: 240 },
+ BOUNDING_BOX_ALPHA = 0.8,
+ HANDLE_NORMAL_COLOR = { red: 0, green: 240, blue: 240 },
+ HANDLE_HOVER_COLOR = { red: 0, green: 255, blue: 120 },
+ HANDLE_NORMAL_ALPHA = 0.7,
+ HANDLE_HOVER_ALPHA = 0.9,
+ NUM_CORNERS = 8,
+ NUM_CORNER_HANDLES = 2,
+ CORNER_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.1, z: 0.1 },
+ CORNER_HANDLE_OVERLAY_AXES,
+ NUM_FACE_HANDLES = 6,
+ FACE_HANDLE_OVERLAY_DIMENSIONS = { x: 0.1, y: 0.12, z: 0.1 },
+ FACE_HANDLE_OVERLAY_AXES,
+ FACE_HANDLE_OVERLAY_OFFSETS,
+ FACE_HANDLE_OVERLAY_ROTATIONS,
+ FACE_HANDLE_OVERLAY_SCALE_AXES,
+ DISTANCE_MULTIPLIER_MULTIPLIER = 0.25,
+ hoveredOverlayID = null,
+ isVisible = false,
+
+ // Scaling.
+ scalingBoundingBoxDimensions,
+ scalingBoundingBoxLocalCenter,
+
+ i;
+
+ if (!(this instanceof Handles)) {
+ return new Handles(side);
+ }
+
+ CORNER_HANDLE_OVERLAY_AXES = [
+ // Ordered such that items 4 apart are opposite corners - used in display().
+ { x: -0.5, y: -0.5, z: -0.5 },
+ { x: -0.5, y: -0.5, z: 0.5 },
+ { x: -0.5, y: 0.5, z: -0.5 },
+ { x: -0.5, y: 0.5, z: 0.5 },
+ { x: 0.5, y: 0.5, z: 0.5 },
+ { x: 0.5, y: 0.5, z: -0.5 },
+ { x: 0.5, y: -0.5, z: 0.5 },
+ { x: 0.5, y: -0.5, z: -0.5 }
+ ];
+
+ FACE_HANDLE_OVERLAY_AXES = [
+ { x: -0.5, y: 0, z: 0 },
+ { x: 0.5, y: 0, z: 0 },
+ { x: 0, y: -0.5, z: 0 },
+ { x: 0, y: 0.5, z: 0 },
+ { x: 0, y: 0, z: -0.5 },
+ { x: 0, y: 0, z: 0.5 }
+ ];
+
+ FACE_HANDLE_OVERLAY_OFFSETS = {
+ x: FACE_HANDLE_OVERLAY_DIMENSIONS.y,
+ y: FACE_HANDLE_OVERLAY_DIMENSIONS.y,
+ z: FACE_HANDLE_OVERLAY_DIMENSIONS.y
+ };
+
+ FACE_HANDLE_OVERLAY_ROTATIONS = [
+ Quat.fromVec3Degrees({ x: 0, y: 0, z: 90 }),
+ Quat.fromVec3Degrees({ x: 0, y: 0, z: -90 }),
+ Quat.fromVec3Degrees({ x: 180, y: 0, z: 0 }),
+ Quat.fromVec3Degrees({ x: 0, y: 0, z: 0 }),
+ Quat.fromVec3Degrees({ x: -90, y: 0, z: 0 }),
+ Quat.fromVec3Degrees({ x: 90, y: 0, z: 0 })
+ ];
+
+ FACE_HANDLE_OVERLAY_SCALE_AXES = [
+ Vec3.UNIT_NEG_X,
+ Vec3.UNIT_X,
+ Vec3.UNIT_NEG_Y,
+ Vec3.UNIT_Y,
+ Vec3.UNIT_NEG_Z,
+ Vec3.UNIT_Z
+ ];
+
+ function isAxisHandle(overlayID) {
+ return faceHandleOverlays.indexOf(overlayID) !== -1;
+ }
+
+ function isCornerHandle(overlayID) {
+ return cornerHandleOverlays.indexOf(overlayID) !== -1;
+ }
+
+ function isHandle(overlayID) {
+ return isAxisHandle(overlayID) || isCornerHandle(overlayID);
+ }
+
+ function handleOffset(overlayID) {
+ // Distance from overlay position to entity surface.
+ if (isCornerHandle(overlayID)) {
+ return 0; // Corner overlays are centered on the corner.
+ }
+ return faceHandleOffsets.y / 2;
+ }
+
+ function getOverlays() {
+ return [].concat(cornerHandleOverlays, faceHandleOverlays);
+ }
+
+ function scalingAxis(overlayID) {
+ var axesIndex;
+ if (isCornerHandle(overlayID)) {
+ axesIndex = CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[cornerHandleOverlays.indexOf(overlayID)]];
+ return Vec3.normalize(Vec3.multiplyVbyV(axesIndex, boundingBoxDimensions));
+ }
+ return FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)];
+ }
+
+ function scalingDirections(overlayID) {
+ if (isCornerHandle(overlayID)) {
+ return Vec3.ONE;
+ }
+ return Vec3.abs(FACE_HANDLE_OVERLAY_SCALE_AXES[faceHandleOverlays.indexOf(overlayID)]);
+ }
+
+ function display(rootEntityID, boundingBox, isMultipleEntities, isSuppressZAxis) {
+ var boundingBoxCenter,
+ boundingBoxOrientation,
+ cameraPosition,
+ boundingBoxVector,
+ distanceMultiplier,
+ cameraUp,
+ cornerPosition,
+ cornerVector,
+ crossProductScale,
+ maxCrossProductScale,
+ rightCornerIndex,
+ leftCornerIndex,
+ cornerHandleDimensions,
+ faceHandleDimensions,
+ i;
+
+ isVisible = true;
+
+ boundingBoxDimensions = boundingBox.dimensions;
+ boundingBoxCenter = boundingBox.center;
+ boundingBoxLocalCenter = boundingBox.localCenter;
+ boundingBoxOrientation = boundingBox.orientation;
+
+ // Selection bounding box.
+ boundingBoxOverlay = Overlays.addOverlay("cube", {
+ parentID: rootEntityID,
+ localPosition: boundingBoxLocalCenter,
+ localRotation: Quat.ZERO,
+ dimensions: boundingBoxDimensions,
+ color: BOUNDING_BOX_COLOR,
+ alpha: BOUNDING_BOX_ALPHA,
+ solid: false,
+ drawInFront: true,
+ ignoreRayIntersection: true,
+ visible: true
+ });
+
+ // Somewhat maintain general angular size of scale handles per bounding box center but make more distance ones
+ // display smaller in order to give comfortable depth cue.
+ cameraPosition = Camera.position;
+ boundingBoxVector = Vec3.subtract(boundingBox.center, Camera.position);
+ distanceMultiplier = Vec3.length(boundingBoxVector);
+ distanceMultiplier = DISTANCE_MULTIPLIER_MULTIPLIER
+ * (distanceMultiplier + (1 - Math.LOG10E * Math.log(distanceMultiplier + 1)));
+
+ // Corner scale handles.
+ // At right-most and opposite corners of bounding box.
+ cameraUp = Quat.getUp(Camera.orientation);
+ maxCrossProductScale = 0;
+ for (i = 0; i < NUM_CORNERS; i++) {
+ cornerPosition = Vec3.sum(boundingBoxCenter,
+ Vec3.multiplyQbyV(boundingBoxOrientation,
+ Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[i], boundingBoxDimensions)));
+ cornerVector = Vec3.subtract(cornerPosition, cameraPosition);
+ crossProductScale = Vec3.dot(Vec3.cross(cornerVector, boundingBoxVector), cameraUp);
+ if (crossProductScale > maxCrossProductScale) {
+ maxCrossProductScale = crossProductScale;
+ rightCornerIndex = i;
+ }
+ }
+ leftCornerIndex = (rightCornerIndex + 4) % NUM_CORNERS;
+ cornerIndexes[0] = leftCornerIndex;
+ cornerIndexes[1] = rightCornerIndex;
+ cornerHandleDimensions = Vec3.multiply(distanceMultiplier, CORNER_HANDLE_OVERLAY_DIMENSIONS);
+ for (i = 0; i < NUM_CORNER_HANDLES; i++) {
+ cornerHandleOverlays[i] = Overlays.addOverlay("sphere", {
+ parentID: rootEntityID,
+ localPosition: Vec3.sum(boundingBoxLocalCenter,
+ Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], boundingBoxDimensions)),
+ localRotation: Quat.ZERO,
+ dimensions: cornerHandleDimensions,
+ color: HANDLE_NORMAL_COLOR,
+ alpha: HANDLE_NORMAL_ALPHA,
+ solid: true,
+ drawInFront: true,
+ ignoreRayIntersection: false,
+ visible: true
+ });
+ }
+
+ // Face scale handles.
+ // Only valid for a single entity because for multiple entities, some may be at an angle relative to the root entity
+ // which would necessitate a (non-existent) shear transform be applied to them when scaling a face of the set.
+ if (!isMultipleEntities) {
+ faceHandleDimensions = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_DIMENSIONS);
+ faceHandleOffsets = Vec3.multiply(distanceMultiplier, FACE_HANDLE_OVERLAY_OFFSETS);
+ for (i = 0; i < NUM_FACE_HANDLES; i++) {
+ if (!isSuppressZAxis || FACE_HANDLE_OVERLAY_AXES[i].z === 0) {
+ faceHandleOverlays[i] = Overlays.addOverlay("shape", {
+ parentID: rootEntityID,
+ localPosition: Vec3.sum(boundingBoxLocalCenter,
+ Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i], Vec3.sum(boundingBoxDimensions, faceHandleOffsets))),
+ localRotation: FACE_HANDLE_OVERLAY_ROTATIONS[i],
+ dimensions: faceHandleDimensions,
+ shape: "Cone",
+ color: HANDLE_NORMAL_COLOR,
+ alpha: HANDLE_NORMAL_ALPHA,
+ solid: true,
+ drawInFront: true,
+ ignoreRayIntersection: false,
+ visible: true
+ });
+ }
+ }
+ } else {
+ faceHandleOverlays = [];
+ }
+ }
+
+ function startScaling() {
+ // Nothing to do.
+ }
+
+ function scale(scale3D) {
+ // Scale relative to dimensions and positions at start of scaling.
+
+ // Selection bounding box.
+ scalingBoundingBoxDimensions = Vec3.multiplyVbyV(scale3D, boundingBoxLocalCenter);
+ scalingBoundingBoxLocalCenter = Vec3.multiplyVbyV(scale3D, boundingBoxDimensions);
+ Overlays.editOverlay(boundingBoxOverlay, {
+ localPosition: scalingBoundingBoxDimensions,
+ dimensions: scalingBoundingBoxLocalCenter
+ });
+
+ // Corner scale handles.
+ for (i = 0; i < NUM_CORNER_HANDLES; i++) {
+ Overlays.editOverlay(cornerHandleOverlays[i], {
+ localPosition: Vec3.sum(scalingBoundingBoxDimensions,
+ Vec3.multiplyVbyV(CORNER_HANDLE_OVERLAY_AXES[cornerIndexes[i]], scalingBoundingBoxLocalCenter))
+ });
+ }
+
+ // Face scale handles.
+ if (faceHandleOverlays.length > 0) {
+ for (i = 0; i < NUM_FACE_HANDLES; i++) {
+ Overlays.editOverlay(faceHandleOverlays[i], {
+ localPosition: Vec3.sum(scalingBoundingBoxDimensions,
+ Vec3.multiplyVbyV(FACE_HANDLE_OVERLAY_AXES[i],
+ Vec3.sum(scalingBoundingBoxLocalCenter, faceHandleOffsets)))
+ });
+ }
+ }
+ }
+
+ function finishScaling() {
+ // Adopt final scale.
+ boundingBoxLocalCenter = scalingBoundingBoxDimensions;
+ boundingBoxDimensions = scalingBoundingBoxLocalCenter;
+ }
+
+ function hover(overlayID) {
+ if (overlayID !== hoveredOverlayID) {
+ if (hoveredOverlayID !== null) {
+ Overlays.editOverlay(hoveredOverlayID, { color: HANDLE_NORMAL_COLOR });
+ hoveredOverlayID = null;
+ }
+
+ if (overlayID !== null
+ && (faceHandleOverlays.indexOf(overlayID) !== -1 || cornerHandleOverlays.indexOf(overlayID) !== -1)) {
+ Overlays.editOverlay(overlayID, {
+ color: HANDLE_HOVER_COLOR,
+ alpha: HANDLE_HOVER_ALPHA
+ });
+ hoveredOverlayID = overlayID;
+ }
+ }
+ }
+
+ function grab(overlayID) {
+ var overlay,
+ isShowAll = overlayID === null,
+ color = isShowAll ? HANDLE_NORMAL_COLOR : HANDLE_HOVER_COLOR,
+ alpha = isShowAll ? HANDLE_NORMAL_ALPHA : HANDLE_HOVER_ALPHA,
+ i,
+ length;
+
+ for (i = 0, length = cornerHandleOverlays.length; i < length; i++) {
+ overlay = cornerHandleOverlays[i];
+ Overlays.editOverlay(overlay, {
+ visible: isVisible && (isShowAll || overlay === overlayID),
+ color: color,
+ alpha: alpha
+ });
+ }
+
+ for (i = 0, length = faceHandleOverlays.length; i < length; i++) {
+ overlay = faceHandleOverlays[i];
+ Overlays.editOverlay(overlay, {
+ visible: isVisible && (isShowAll || overlay === overlayID),
+ color: color,
+ alpha: alpha
+ });
+ }
+ }
+
+ function clear() {
+ var i,
+ length;
+
+ Overlays.deleteOverlay(boundingBoxOverlay);
+ for (i = 0; i < NUM_CORNER_HANDLES; i++) {
+ Overlays.deleteOverlay(cornerHandleOverlays[i]);
+ }
+ for (i = 0, length = faceHandleOverlays.length; i < length; i++) {
+ Overlays.deleteOverlay(faceHandleOverlays[i]);
+ }
+
+ isVisible = false;
+ }
+
+ function destroy() {
+ clear();
+ }
+
+ return {
+ display: display,
+ overlays: getOverlays,
+ isHandle: isHandle,
+ handleOffset: handleOffset,
+ scalingAxis: scalingAxis,
+ scalingDirections: scalingDirections,
+ startScaling: startScaling,
+ scale: scale,
+ finishScaling: finishScaling,
+ hover: hover,
+ grab: grab,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+Handles.prototype = {};
diff --git a/scripts/shapes/modules/highlights.js b/scripts/shapes/modules/highlights.js
new file mode 100644
index 0000000000..98c200a808
--- /dev/null
+++ b/scripts/shapes/modules/highlights.js
@@ -0,0 +1,167 @@
+//
+// highlights.js
+//
+// Created by David Rowe on 21 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Highlights: true */
+
+Highlights = function (side) {
+ // Draws highlights on selected entities.
+
+ "use strict";
+
+ var handOverlay,
+ entityOverlays = [],
+ boundingBoxOverlay,
+ isDisplayingBoundingBox = false,
+ HIGHLIGHT_COLOR = { red: 240, green: 240, blue: 0 },
+ SCALE_COLOR = { red: 0, green: 240, blue: 240 },
+ GROUP_COLOR = { red: 220, green: 60, blue: 220 },
+ HAND_HIGHLIGHT_ALPHA = 0.35,
+ ENTITY_HIGHLIGHT_ALPHA = 0.8,
+ BOUNDING_BOX_ALPHA = 0.8,
+ HAND_HIGHLIGHT_OFFSET = { x: 0.0, y: 0.11, z: 0.02 },
+ LEFT_HAND = 0;
+
+ if (!(this instanceof Highlights)) {
+ return new Highlights();
+ }
+
+ handOverlay = Overlays.addOverlay("sphere", {
+ dimension: Vec3.ZERO,
+ parentID: Uuid.SELF,
+ parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND
+ ? "_CONTROLLER_LEFTHAND"
+ : "_CONTROLLER_RIGHTHAND"),
+ localPosition: HAND_HIGHLIGHT_OFFSET,
+ alpha: HAND_HIGHLIGHT_ALPHA,
+ solid: true,
+ drawInFront: true,
+ ignoreRayIntersection: true,
+ visible: false
+ });
+
+ boundingBoxOverlay = Overlays.addOverlay("cube", {
+ alpha: BOUNDING_BOX_ALPHA,
+ solid: false,
+ drawInFront: true,
+ ignoreRayIntersection: true,
+ visible: false
+ });
+
+ function setHandHighlightRadius(radius) {
+ var dimension = 2 * radius;
+ Overlays.editOverlay(handOverlay, {
+ dimensions: { x: dimension, y: dimension, z: dimension }
+ });
+ }
+
+ function maybeAddEntityOverlay(index) {
+ if (index >= entityOverlays.length) {
+ entityOverlays.push(Overlays.addOverlay("cube", {
+ alpha: ENTITY_HIGHLIGHT_ALPHA,
+ solid: false,
+ drawInFront: true,
+ ignoreRayIntersection: true,
+ visible: false
+ }));
+ }
+ }
+
+ function editEntityOverlay(index, details, overlayColor) {
+ var offset = Vec3.multiplyVbyV(Vec3.subtract(Vec3.HALF, details.registrationPoint), details.dimensions);
+
+ Overlays.editOverlay(entityOverlays[index], {
+ parentID: details.id,
+ localPosition: offset,
+ localRotation: Quat.ZERO,
+ dimensions: details.dimensions,
+ color: overlayColor,
+ visible: true
+ });
+ }
+
+ function display(handIntersected, selection, entityIndex, boundingBox, overlayColor) {
+ // Displays highlight for just entityIndex if non-null, otherwise highlights whole selection.
+ var i,
+ length;
+
+ // Show/hide hand overlay.
+ Overlays.editOverlay(handOverlay, {
+ color: overlayColor,
+ visible: handIntersected
+ });
+
+ // Display entity overlays.
+ if (entityIndex !== null) {
+ // Add/edit entity overlay for just entityIndex.
+ maybeAddEntityOverlay(0);
+ editEntityOverlay(0, selection[entityIndex], overlayColor);
+ } else {
+ // Add/edit entity overlays for all entities in selection.
+ for (i = 0, length = selection.length; i < length; i++) {
+ maybeAddEntityOverlay(i);
+ editEntityOverlay(i, selection[i], overlayColor);
+ }
+ }
+
+ // Delete extra entity overlays.
+ for (i = entityOverlays.length - 1, length = selection.length; i >= length; i--) {
+ Overlays.deleteOverlay(entityOverlays[i]);
+ entityOverlays.splice(i, 1);
+ }
+
+ // Update bounding box overlay.
+ if (boundingBox !== null) {
+ Overlays.editOverlay(boundingBoxOverlay, {
+ position: boundingBox.center,
+ rotation: boundingBox.orientation,
+ dimensions: boundingBox.dimensions,
+ color: overlayColor,
+ visible: true
+ });
+ isDisplayingBoundingBox = true;
+ } else if (isDisplayingBoundingBox) {
+ Overlays.editOverlay(boundingBoxOverlay, { visible: false });
+ isDisplayingBoundingBox = false;
+ }
+ }
+
+ function clear() {
+ var i,
+ length;
+
+ // Hide hand and bounding box overlays.
+ Overlays.editOverlay(handOverlay, { visible: false });
+ Overlays.editOverlay(boundingBoxOverlay, { visible: false });
+
+ // Delete entity overlays.
+ for (i = 0, length = entityOverlays.length; i < length; i++) {
+ Overlays.deleteOverlay(entityOverlays[i]);
+ }
+ entityOverlays = [];
+ }
+
+ function destroy() {
+ clear();
+ Overlays.deleteOverlay(handOverlay);
+ Overlays.deleteOverlay(boundingBoxOverlay);
+ }
+
+ return {
+ HIGHLIGHT_COLOR: HIGHLIGHT_COLOR,
+ SCALE_COLOR: SCALE_COLOR,
+ GROUP_COLOR: GROUP_COLOR,
+ setHandHighlightRadius: setHandHighlightRadius,
+ display: display,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+Highlights.prototype = {};
diff --git a/scripts/shapes/modules/history.js b/scripts/shapes/modules/history.js
new file mode 100644
index 0000000000..9f2c45046a
--- /dev/null
+++ b/scripts/shapes/modules/history.js
@@ -0,0 +1,246 @@
+//
+// history.js
+//
+// Created by David Rowe on 12 Sep 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global History: true */
+
+History = (function () {
+ // Provides undo facility.
+ // Global object.
+
+ "use strict";
+
+ var history = [
+ /*
+ {
+ undoData: {
+ setProperties: [
+ {
+ entityID: ,
+ properties: { : , : , ... }
+ }
+ ],
+ createEntities: [
+ {
+ entityID: ,
+ properties: { : , : , ... }
+ }
+ ],
+ deleteEntities: [
+ {
+ entityID: ,
+ properties: { : , : , ... }
+ }
+ ]
+ },
+ redoData: {
+ ""
+ }
+ }
+ */
+ ],
+ MAX_HISTORY_ITEMS = 1000,
+ undoPosition = -1, // The next history item to undo; the next history item to redo = undoIndex + 1.
+ undoData = [{}, {}, {}], // Left side, right side, no side.
+ redoData = [{}, {}, {}],
+ NO_SIDE = 2;
+
+ function doKick(entityID) {
+ var properties,
+ NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick.
+ DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD
+ DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 };
+
+ properties = Entities.getEntityProperties(entityID, ["type", "dynamic", "gravity", "velocity"]);
+ if (NO_KICK_ENTITY_TYPES.indexOf(properties.type) === -1 && properties.dynamic
+ && Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) {
+ Entities.editEntity(entityID, { velocity: DYNAMIC_VELOCITY_KICK });
+ }
+ }
+
+ function kickPhysics(entityID) {
+ // Gives entities a small kick to start off physics, if necessary.
+ var KICK_DELAY = 750; // ms
+
+ // Give physics a chance to catch up. Avoids some erratic behavior.
+ Script.setTimeout(function () {
+ doKick(entityID);
+ }, KICK_DELAY);
+ }
+
+ function prePush(side, undo, redo) {
+ // Stores undo and redo data to include in the next history entry generated for the side.
+ undoData[side] = undo;
+ redoData[side] = redo;
+ }
+
+ function push(side, undo, redo) {
+ // Add a history entry.
+ if (side === null) {
+ side = NO_SIDE;
+ }
+ undoData[side] = Object.merge(undoData[side], undo);
+ redoData[side] = Object.merge(redoData[side], redo);
+
+ // Wipe any redo history after current undo position.
+ if (undoPosition < history.length - 1) {
+ history.splice(undoPosition + 1, history.length - undoPosition - 1);
+ }
+
+ // Limit the number of history items.
+ if (history.length >= MAX_HISTORY_ITEMS) {
+ history.splice(0, history.length - MAX_HISTORY_ITEMS + 1);
+ undoPosition = history.length - 1;
+ }
+
+ history.push({ undoData: undoData[side], redoData: redoData[side] });
+ undoPosition++;
+
+ undoData[side] = {};
+ redoData[side] = {};
+ }
+
+ function updateEntityIDs(oldEntityID, newEntityID) {
+ // Replace oldEntityID value with newEntityID in history.
+ var i,
+ length;
+
+ function updateEntityIDsInProperty(properties) {
+ var i,
+ length;
+
+ if (properties) {
+ for (i = 0, length = properties.length; i < length; i++) {
+ if (properties[i].entityID === oldEntityID) {
+ properties[i].entityID = newEntityID;
+ }
+ if (properties[i].properties && properties[i].properties.parentID === oldEntityID) {
+ properties[i].properties.parentID = newEntityID;
+ }
+ }
+ }
+ }
+
+ for (i = 0, length = history.length; i < length; i++) {
+ if (history[i].undoData) {
+ updateEntityIDsInProperty(history[i].undoData.setProperties);
+ updateEntityIDsInProperty(history[i].undoData.createEntities);
+ updateEntityIDsInProperty(history[i].undoData.deleteEntities);
+ }
+ if (history[i].redoData) {
+ updateEntityIDsInProperty(history[i].redoData.setProperties);
+ updateEntityIDsInProperty(history[i].redoData.createEntities);
+ updateEntityIDsInProperty(history[i].redoData.deleteEntities);
+ }
+ }
+ }
+
+ function hasUndo() {
+ return undoPosition > -1;
+ }
+
+ function hasRedo() {
+ return undoPosition < history.length - 1;
+ }
+
+ function undoSetProperties(entityID, properties) {
+ Entities.editEntity(entityID, properties);
+ if (properties.gravity) {
+ kickPhysics(entityID);
+ }
+ }
+
+ function undo() {
+ var undoData,
+ entityID,
+ REPEAT_UNDO_DELAY = 500, // ms
+ i,
+ length;
+
+ if (undoPosition > -1) {
+ undoData = history[undoPosition].undoData;
+
+ if (undoData.createEntities) {
+ for (i = 0, length = undoData.createEntities.length; i < length; i++) {
+ entityID = Entities.addEntity(undoData.createEntities[i].properties);
+ updateEntityIDs(undoData.createEntities[i].entityID, entityID);
+ }
+ }
+
+ if (undoData.setProperties) {
+ for (i = 0, length = undoData.setProperties.length; i < length; i++) {
+ undoSetProperties(undoData.setProperties[i].entityID, undoData.setProperties[i].properties);
+ if (undoData.setProperties[i].properties.velocity
+ && Vec3.equal(undoData.setProperties[i].properties.velocity, Vec3.ZERO)
+ && undoData.setProperties[i].properties.angularVelocity
+ && Vec3.equal(undoData.setProperties[i].properties.angularVelocity, Vec3.ZERO)) {
+ // Work around physics bug wherein the entity doesn't always end up at the correct position.
+ Script.setTimeout(
+ undoSetProperties(undoData.setProperties[i].entityID, undoData.setProperties[i].properties),
+ REPEAT_UNDO_DELAY
+ );
+ }
+ }
+ }
+
+ if (undoData.deleteEntities) {
+ for (i = 0, length = undoData.deleteEntities.length; i < length; i++) {
+ Entities.deleteEntity(undoData.deleteEntities[i].entityID);
+ }
+ }
+
+ undoPosition--;
+ }
+ }
+
+ function redo() {
+ var redoData,
+ entityID,
+ i,
+ length;
+
+
+ if (undoPosition < history.length - 1) {
+ redoData = history[undoPosition + 1].redoData;
+
+ if (redoData.createEntities) {
+ for (i = 0, length = redoData.createEntities.length; i < length; i++) {
+ entityID = Entities.addEntity(redoData.createEntities[i].properties);
+ updateEntityIDs(redoData.createEntities[i].entityID, entityID);
+ }
+ }
+
+ if (redoData.setProperties) {
+ for (i = 0, length = redoData.setProperties.length; i < length; i++) {
+ Entities.editEntity(redoData.setProperties[i].entityID, redoData.setProperties[i].properties);
+ if (redoData.setProperties[i].properties.gravity) {
+ kickPhysics(redoData.setProperties[i].entityID);
+ }
+ }
+ }
+
+ if (redoData.deleteEntities) {
+ for (i = 0, length = redoData.deleteEntities.length; i < length; i++) {
+ Entities.deleteEntity(redoData.deleteEntities[i].entityID);
+ }
+ }
+
+ undoPosition++;
+ }
+ }
+
+ return {
+ prePush: prePush,
+ push: push,
+ hasUndo: hasUndo,
+ hasRedo: hasRedo,
+ undo: undo,
+ redo: redo
+ };
+}());
diff --git a/scripts/shapes/modules/laser.js b/scripts/shapes/modules/laser.js
new file mode 100644
index 0000000000..1efc38b65a
--- /dev/null
+++ b/scripts/shapes/modules/laser.js
@@ -0,0 +1,287 @@
+//
+// laser.js
+//
+// Created by David Rowe on 21 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Laser:true */
+
+Laser = function (side) {
+ // Draws hand lasers.
+ // May intersect with overlays or entities, or bounding box of other hand's selection.
+ // Laser dot is always drawn on UI entities.
+
+ "use strict";
+
+ var isLaserEnabled = true,
+ isLaserOn = false,
+
+ laserLine = null,
+ laserSphere = null,
+
+ searchDistance = 0.0,
+
+ SEARCH_SPHERE_SIZE = 0.013, // Per farActionGrabEntity.js multiplied by 1.2 per farActionGrabEntity.js.
+ MINUMUM_SEARCH_SPHERE_SIZE = 0.006,
+ SEARCH_SPHERE_FOLLOW_RATE = 0.5,
+ COLORS_GRAB_SEARCHING_HALF_SQUEEZE = { red: 10, green: 10, blue: 255 }, // Per controllerDispatcherUtils.js.
+ COLORS_GRAB_SEARCHING_FULL_SQUEEZE = { red: 250, green: 10, blue: 10 }, // Per controllerDispatcherUtils.js.
+ COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT,
+ COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT,
+ BRIGHT_POW = 0.06, // Per old handControllerGrab.js.
+
+ GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }, // Per HmdDisplayPlugin.cpp and controllers.js.
+
+ PICK_MAX_DISTANCE = 500, // Per controllerDispatcherUtils.js.
+ PRECISION_PICKING = true,
+ NO_INCLUDE_IDS = [],
+ NO_EXCLUDE_IDS = [],
+ VISIBLE_ONLY = true,
+
+ laserLength,
+ specifiedLaserLength = null,
+ laserSphereSize = 0,
+
+ LEFT_HAND = 0,
+
+ uiOverlayIDs = [],
+
+ intersection;
+
+ if (!(this instanceof Laser)) {
+ return new Laser(side);
+ }
+
+ function colorPow(color, power) { // Per old handControllerGrab.js.
+ return {
+ red: Math.pow(color.red / 255, power) * 255,
+ green: Math.pow(color.green / 255, power) * 255,
+ blue: Math.pow(color.blue / 255, power) * 255
+ };
+ }
+
+ COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_HALF_SQUEEZE, BRIGHT_POW);
+ COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT = colorPow(COLORS_GRAB_SEARCHING_FULL_SQUEEZE, BRIGHT_POW);
+
+ if (side === LEFT_HAND) {
+ GRAB_POINT_SPHERE_OFFSET.x = -GRAB_POINT_SPHERE_OFFSET.x;
+ }
+
+ laserLine = Overlays.addOverlay("line3d", {
+ lineWidth: 5,
+ alpha: 1.0,
+ glow: 1.0,
+ ignoreRayIntersection: true,
+ drawInFront: true,
+ parentID: Uuid.SELF,
+ parentJointIndex: MyAvatar.getJointIndex(side === LEFT_HAND
+ ? "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"
+ : "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"),
+ visible: false
+ });
+ laserSphere = Overlays.addOverlay("circle3d", {
+ innerAlpha: 1.0,
+ outerAlpha: 0.0,
+ solid: true,
+ ignoreRayIntersection: true,
+ drawInFront: true,
+ visible: false
+ });
+
+ function updateLine(start, end, color) {
+ Overlays.editOverlay(laserLine, {
+ start: start,
+ end: end,
+ color: color,
+ visible: true
+ });
+ }
+
+ function updateSphere(location, size, color, brightColor) {
+ var rotation;
+
+ rotation = Quat.lookAt(location, Camera.getPosition(), Vec3.UP);
+
+ Overlays.editOverlay(laserSphere, {
+ position: location,
+ rotation: rotation,
+ innerColor: brightColor,
+ outerColor: color,
+ outerRadius: size,
+ visible: true
+ });
+ }
+
+ function display(origin, direction, distance, isPressed, isClicked) {
+ var searchTarget,
+ sphereSize,
+ color,
+ brightColor;
+
+ searchDistance = SEARCH_SPHERE_FOLLOW_RATE * searchDistance + (1.0 - SEARCH_SPHERE_FOLLOW_RATE) * distance;
+ searchTarget = Vec3.sum(origin, Vec3.multiply(searchDistance, direction));
+ sphereSize = Math.max(SEARCH_SPHERE_SIZE * searchDistance, MINUMUM_SEARCH_SPHERE_SIZE);
+ color = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : COLORS_GRAB_SEARCHING_HALF_SQUEEZE;
+ brightColor = isClicked ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE_BRIGHT : COLORS_GRAB_SEARCHING_HALF_SQUEEZE_BRIGHT;
+
+ if (isPressed) {
+ updateLine(origin, searchTarget, color);
+ } else {
+ Overlays.editOverlay(laserLine, { visible: false });
+ }
+ // Avoid flash from large laser sphere when turn on or suddenly increase distance. Rendering seems to update overlay
+ // position one frame behind so use sphere size from preceding frame.
+ updateSphere(searchTarget, laserSphereSize, color, brightColor);
+ laserSphereSize = sphereSize;
+ }
+
+ function hide() {
+ Overlays.editOverlay(laserLine, { visible: false });
+ Overlays.editOverlay(laserSphere, { visible: false });
+ laserSphereSize = 0;
+ }
+
+ function setUIOverlays(overlayIDs) {
+ uiOverlayIDs = overlayIDs;
+ }
+
+ function update(hand) {
+ var handPosition,
+ handOrientation,
+ deltaOrigin,
+ pickRay;
+
+ if (!isLaserEnabled) {
+ intersection = {};
+ return;
+ }
+
+ handPosition = hand.position();
+ handOrientation = hand.orientation();
+ deltaOrigin = Vec3.multiplyQbyV(handOrientation, GRAB_POINT_SPHERE_OFFSET);
+ pickRay = {
+ origin: Vec3.sum(handPosition, deltaOrigin),
+ direction: Quat.getUp(handOrientation),
+ length: PICK_MAX_DISTANCE
+ };
+
+ if (hand.triggerPressed()) {
+
+ // Normal laser operation with trigger.
+ intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS,
+ VISIBLE_ONLY);
+ if (Reticle.pointingAtSystemOverlay || (intersection.overlayID
+ && [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID].indexOf(intersection.overlayID) !== -1)) {
+ // No laser if pointing at HUD overlay or tablet; system provides lasers for these cases.
+ if (isLaserOn) {
+ isLaserOn = false;
+ hide();
+ }
+ } else {
+ if (!intersection.intersects) {
+ intersection = Entities.findRayIntersection(pickRay, PRECISION_PICKING, NO_INCLUDE_IDS, NO_EXCLUDE_IDS,
+ VISIBLE_ONLY);
+ intersection.editableEntity = intersection.intersects && Entities.hasEditableRoot(intersection.entityID);
+ intersection.overlayID = null;
+ }
+ intersection.laserIntersected = intersection.intersects;
+ laserLength = (specifiedLaserLength !== null)
+ ? specifiedLaserLength
+ : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE);
+ isLaserOn = true;
+ display(pickRay.origin, pickRay.direction, laserLength, true, hand.triggerClicked());
+ }
+
+ } else if (uiOverlayIDs.length > 0) {
+
+ // Special UI cursor.
+ intersection = Overlays.findRayIntersection(pickRay, PRECISION_PICKING, uiOverlayIDs, NO_EXCLUDE_IDS,
+ VISIBLE_ONLY);
+ if (intersection.intersects) {
+ intersection.laserIntersected = true;
+ laserLength = (specifiedLaserLength !== null)
+ ? specifiedLaserLength
+ : (intersection.intersects ? intersection.distance : PICK_MAX_DISTANCE);
+ if (!isLaserOn) {
+ // Start laser dot at UI distance.
+ searchDistance = laserLength;
+ }
+ isLaserOn = true;
+ display(pickRay.origin, pickRay.direction, laserLength, false, false);
+ } else if (isLaserOn) {
+ isLaserOn = false;
+ hide();
+ }
+
+ } else {
+ intersection = { intersects: false };
+ if (isLaserOn) {
+ isLaserOn = false;
+ hide();
+ }
+ }
+ }
+
+ function getIntersection() {
+ return intersection;
+ }
+
+ function setLength(length) {
+ specifiedLaserLength = length;
+ laserLength = length;
+ }
+
+ function clearLength() {
+ specifiedLaserLength = null;
+ }
+
+ function getLength() {
+ return laserLength;
+ }
+
+ function handOffset() {
+ return GRAB_POINT_SPHERE_OFFSET;
+ }
+
+ function clear() {
+ isLaserOn = false;
+ hide();
+ }
+
+ function enable() {
+ isLaserEnabled = true;
+ }
+
+ function disable() {
+ isLaserEnabled = false;
+ if (isLaserOn) {
+ hide();
+ }
+ isLaserOn = false;
+ }
+
+ function destroy() {
+ Overlays.deleteOverlay(laserLine);
+ Overlays.deleteOverlay(laserSphere);
+ }
+
+ return {
+ setUIOverlays: setUIOverlays,
+ update: update,
+ intersection: getIntersection,
+ setLength: setLength,
+ clearLength: clearLength,
+ length: getLength,
+ enable: enable,
+ disable: disable,
+ handOffset: handOffset,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+Laser.prototype = {};
diff --git a/scripts/shapes/modules/selection.js b/scripts/shapes/modules/selection.js
new file mode 100644
index 0000000000..955cde6bda
--- /dev/null
+++ b/scripts/shapes/modules/selection.js
@@ -0,0 +1,795 @@
+//
+// selection.js
+//
+// Created by David Rowe on 21 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global SelectionManager:true, App, History */
+
+SelectionManager = function (side) {
+ // Manages set of selected entities. Currently supports just one set of linked entities.
+
+ "use strict";
+
+ var selection = [], // Subset of properties to provide externally.
+ selectionIDs = [],
+ selectionProperties = [], // Full set of properties for history.
+ intersectedEntityID = null,
+ intersectedEntityIndex,
+ rootEntityID = null,
+ rootPosition,
+ rootOrientation,
+ scaleFactor,
+ scaleRootOffset,
+ scaleRootOrientation,
+ startPosition,
+ startOrientation,
+ isEditing = false,
+ ENTITY_TYPE = "entity",
+ ENTITY_TYPES_WITH_COLOR = ["Box", "Sphere", "Shape", "PolyLine"],
+ ENTITY_TYPES_2D = ["Text", "Web"],
+ MIN_HISTORY_MOVE_DISTANCE = 0.005,
+ MIN_HISTORY_ROTATE_ANGLE = 0.017453; // Radians = 1 degree.
+
+ if (!(this instanceof SelectionManager)) {
+ return new SelectionManager(side);
+ }
+
+ function traverseEntityTree(id, selection, selectionIDs, selectionProperties) {
+ // Recursively traverses tree of entities and their children, gather IDs and properties.
+ // The root entity is always the first entry.
+ var children,
+ properties,
+ i,
+ length;
+
+ properties = Entities.getEntityProperties(id);
+ delete properties.entityID;
+ selection.push({
+ id: id,
+ type: properties.type,
+ position: properties.position,
+ parentID: properties.parentID,
+ localPosition: properties.localPosition,
+ localRotation: properties.localRotation,
+ registrationPoint: properties.registrationPoint,
+ rotation: properties.rotation,
+ dimensions: properties.dimensions,
+ dynamic: properties.dynamic,
+ collisionless: properties.collisionless,
+ userData: properties.userData
+ });
+ selectionIDs.push(id);
+ selectionProperties.push({ entityID: id, properties: properties });
+
+ if (id === intersectedEntityID) {
+ intersectedEntityIndex = selection.length - 1;
+ }
+
+ children = Entities.getChildrenIDs(id);
+ for (i = 0, length = children.length; i < length; i++) {
+ if (Entities.getNestableType(children[i]) === ENTITY_TYPE) {
+ traverseEntityTree(children[i], selection, selectionIDs, selectionProperties);
+ }
+ }
+ }
+
+ function select(intersectionEntityID) {
+ var entityProperties,
+ PARENT_PROPERTIES = ["parentID", "position", "rotation", "dymamic", "collisionless"];
+
+ intersectedEntityID = intersectionEntityID;
+
+ // Find root parent.
+ rootEntityID = Entities.rootOf(intersectedEntityID);
+
+ // Selection position and orientation is that of the root entity.
+ entityProperties = Entities.getEntityProperties(rootEntityID, PARENT_PROPERTIES);
+ rootPosition = entityProperties.position;
+ rootOrientation = entityProperties.rotation;
+
+ // Find all children.
+ selection = [];
+ selectionIDs = [];
+ selectionProperties = [];
+ traverseEntityTree(rootEntityID, selection, selectionIDs, selectionProperties);
+ }
+
+ function append(rootEntityID) {
+ // Add further entities to the selection.
+ // Assumes that rootEntityID is not already in the selection.
+ traverseEntityTree(rootEntityID, selection, selectionIDs, selectionProperties);
+ }
+
+ function getIntersectedEntityID() {
+ return intersectedEntityID;
+ }
+
+ function getIntersectedEntityIndex() {
+ return intersectedEntityIndex;
+ }
+
+ function getRootEntityID() {
+ return rootEntityID;
+ }
+
+ function getSelection() {
+ return selection;
+ }
+
+ function contains(entityID) {
+ return selectionIDs.indexOf(entityID) !== -1;
+ }
+
+ function count() {
+ return selection.length;
+ }
+
+ function getBoundingBox() {
+ var center,
+ localCenter,
+ orientation,
+ inverseOrientation,
+ dimensions,
+ min,
+ max,
+ i,
+ j,
+ length,
+ registration,
+ position,
+ rotation,
+ corners = [],
+ NUM_CORNERS = 8;
+
+ if (selection.length === 1) {
+ if (Vec3.equal(selection[0].registrationPoint, Vec3.HALF)) {
+ center = rootPosition;
+ } else {
+ center = Vec3.sum(rootPosition,
+ Vec3.multiplyQbyV(rootOrientation,
+ Vec3.multiplyVbyV(selection[0].dimensions,
+ Vec3.subtract(Vec3.HALF, selection[0].registrationPoint))));
+ }
+ localCenter = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(center, rootPosition));
+ orientation = rootOrientation;
+ dimensions = selection[0].dimensions;
+ } else if (selection.length > 1) {
+ // Find min & max x, y, z values of entities' dimension box corners in root entity coordinate system.
+ // Note: Don't use entities' bounding boxes because they're in world coordinates and may make the calculated
+ // bounding box be larger than necessary.
+ min = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ZERO, selection[0].registrationPoint), selection[0].dimensions);
+ max = Vec3.multiplyVbyV(Vec3.subtract(Vec3.ONE, selection[0].registrationPoint), selection[0].dimensions);
+ inverseOrientation = Quat.inverse(rootOrientation);
+ for (i = 1, length = selection.length; i < length; i++) {
+
+ registration = selection[i].registrationPoint;
+ corners = [
+ { x: -registration.x, y: -registration.y, z: -registration.z },
+ { x: -registration.x, y: -registration.y, z: 1.0 - registration.z },
+ { x: -registration.x, y: 1.0 - registration.y, z: -registration.z },
+ { x: -registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z },
+ { x: 1.0 - registration.x, y: -registration.y, z: -registration.z },
+ { x: 1.0 - registration.x, y: -registration.y, z: 1.0 - registration.z },
+ { x: 1.0 - registration.x, y: 1.0 - registration.y, z: -registration.z },
+ { x: 1.0 - registration.x, y: 1.0 - registration.y, z: 1.0 - registration.z }
+ ];
+
+ position = selection[i].position;
+ rotation = selection[i].rotation;
+ dimensions = selection[i].dimensions;
+
+ for (j = 0; j < NUM_CORNERS; j++) {
+ // Corner position in world coordinates.
+ corners[j] = Vec3.sum(position, Vec3.multiplyQbyV(rotation, Vec3.multiplyVbyV(corners[j], dimensions)));
+ // Corner position in root entity coordinates.
+ corners[j] = Vec3.multiplyQbyV(inverseOrientation, Vec3.subtract(corners[j], rootPosition));
+ // Update min & max.
+ min = Vec3.min(corners[j], min);
+ max = Vec3.max(corners[j], max);
+ }
+ }
+
+ // Calculate bounding box.
+ center = Vec3.sum(rootPosition,
+ Vec3.multiplyQbyV(rootOrientation, Vec3.multiply(0.5, Vec3.sum(min, max))));
+ localCenter = Vec3.multiply(0.5, Vec3.sum(min, max));
+ orientation = rootOrientation;
+ dimensions = Vec3.subtract(max, min);
+ }
+
+ return {
+ center: center,
+ localCenter: localCenter,
+ orientation: orientation,
+ dimensions: dimensions
+ };
+ }
+
+ function is2D() {
+ return selection.length === 1 && ENTITY_TYPES_2D.indexOf(selection[0].type) !== -1;
+ }
+
+ function doKick(entityID) {
+ var properties,
+ NO_KICK_ENTITY_TYPES = ["Text", "Web", "PolyLine", "ParticleEffect"], // Don't respond to gravity so don't kick.
+ DYNAMIC_VELOCITY_THRESHOLD = 0.05, // See EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD
+ DYNAMIC_VELOCITY_KICK = { x: 0, y: 0.1, z: 0 };
+
+ if (entityID === rootEntityID && isEditing) {
+ // Don't kick if have started editing entity again.
+ return;
+ }
+
+ properties = Entities.getEntityProperties(entityID, ["type", "velocity", "gravity"]);
+ if (NO_KICK_ENTITY_TYPES.indexOf(properties.type) === -1
+ && Vec3.length(properties.gravity) > 0 && Vec3.length(properties.velocity) < DYNAMIC_VELOCITY_THRESHOLD) {
+ Entities.editEntity(entityID, { velocity: DYNAMIC_VELOCITY_KICK });
+ }
+ }
+
+ function kickPhysics(entityID) {
+ // Gives entities a small kick to start off physics, if necessary.
+ var KICK_DELAY = 750; // ms
+
+ // Give physics a chance to catch up. Avoids some erratic behavior.
+ Script.setTimeout(function () {
+ doKick(entityID);
+ }, KICK_DELAY);
+ }
+
+ function startEditing() {
+ var i;
+
+ // Remember start properties for history entry.
+ startPosition = selection[0].position;
+ startOrientation = selection[0].rotation;
+
+ // Disable entity set's physics.
+ for (i = selection.length - 1; i >= 0; i--) {
+ Entities.editEntity(selection[i].id, {
+ dynamic: false, // So that gravity doesn't fight with us trying to hold the entity in place.
+ collisionless: true, // So that entity doesn't bump us about as we resize the entity.
+ velocity: Vec3.ZERO, // So that entity doesn't drift if we've grabbed a set while it was moving.
+ angularVelocity: Vec3.ZERO // ""
+ });
+ }
+
+ // Stop moving.
+ Entities.editEntity(rootEntityID, { velocity: Vec3.ZERO, angularVelocity: Vec3.ZERO });
+
+ isEditing = true;
+ }
+
+ function finishEditing() {
+ var i;
+
+ // Restore entity set's physics.
+ // Note: Need to apply children-first in order to avoid children's relative positions sometimes drifting.
+ for (i = selection.length - 1; i >= 0; i--) {
+ Entities.editEntity(selection[i].id, {
+ dynamic: selection[i].dynamic,
+ collisionless: selection[i].collisionless
+ });
+ }
+
+ // Add history entry.
+ if (selection.length > 0
+ && (!Vec3.equal(startPosition, rootPosition) || !Quat.equal(startOrientation, rootOrientation))) {
+ // Positions and orientations can be identical if change grabbing hands when finish scaling.
+ History.push(
+ side,
+ {
+ setProperties: [
+ { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } }
+ ]
+ },
+ {
+ setProperties: [
+ { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } }
+ ]
+ }
+ );
+ }
+
+ // Kick off physics if necessary.
+ if (selection.length > 0 && selection[0].dynamic) {
+ kickPhysics(selection[0].id);
+ }
+
+ isEditing = false;
+ }
+
+ function getPositionAndOrientation() {
+ // Position and orientation of root entity.
+ return {
+ position: rootPosition,
+ orientation: rootOrientation
+ };
+ }
+
+ function setPositionAndOrientation(position, orientation) {
+ // Position and orientation of root entity.
+ rootPosition = position;
+ rootOrientation = orientation;
+ Entities.editEntity(rootEntityID, {
+ position: position,
+ rotation: orientation
+ });
+ }
+
+ function startDirectScaling(center) {
+ // Save initial position and orientation so that can scale relative to these without accumulating float errors.
+ scaleRootOffset = Vec3.subtract(rootPosition, center);
+ scaleRootOrientation = rootOrientation;
+
+ // User is grabbing entity; add a history entry for movement up until the start of scaling and update start position and
+ // orientation; unless very small movement.
+ if (Vec3.distance(startPosition, rootPosition) >= MIN_HISTORY_MOVE_DISTANCE
+ || Quat.rotationBetween(startOrientation, rootOrientation) >= MIN_HISTORY_ROTATE_ANGLE) {
+ History.push(
+ side,
+ {
+ setProperties: [
+ { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } }
+ ]
+ },
+ {
+ setProperties: [
+ { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } }
+ ]
+ }
+ );
+ startPosition = rootPosition;
+ startOrientation = rootOrientation;
+ }
+ }
+
+ function directScale(factor, rotation, center) {
+ // Scale, position, and rotate selection.
+ // We can get away with scaling the z size of 2D entities - incongruities are barely noticeable and things recover.
+ var i,
+ length;
+
+ // Scale, position, and orient root.
+ rootPosition = Vec3.sum(center, Vec3.multiply(factor, Vec3.multiplyQbyV(rotation, scaleRootOffset)));
+ rootOrientation = Quat.multiply(rotation, scaleRootOrientation);
+ Entities.editEntity(selection[0].id, {
+ dimensions: Vec3.multiply(factor, selection[0].dimensions),
+ position: rootPosition,
+ rotation: rootOrientation
+ });
+
+ // Scale and position children.
+ for (i = 1, length = selection.length; i < length; i++) {
+ Entities.editEntity(selection[i].id, {
+ dimensions: Vec3.multiply(factor, selection[i].dimensions),
+ localPosition: Vec3.multiply(factor, selection[i].localPosition),
+ localRotation: selection[i].localRotation // Always specify localRotation otherwise rotations can drift.
+ });
+ }
+
+ // Save most recent scale factor.
+ scaleFactor = factor;
+ }
+
+ function finishDirectScaling() {
+ // Update selection with final entity properties.
+ var undoData = [],
+ redoData = [],
+ i,
+ length;
+
+ // Final scale, position, and orientation of root.
+ undoData.push({
+ entityID: selection[0].id,
+ properties: {
+ dimensions: selection[0].dimensions,
+ position: startPosition,
+ rotation: startOrientation
+ }
+ });
+ selection[0].dimensions = Vec3.multiply(scaleFactor, selection[0].dimensions);
+ selection[0].position = rootPosition;
+ selection[0].rotation = rootOrientation;
+ redoData.push({
+ entityID: selection[0].id,
+ properties: {
+ dimensions: selection[0].dimensions,
+ position: selection[0].position,
+ rotation: selection[0].rotation
+ }
+ });
+
+ // Final scale and position of children.
+ for (i = 1, length = selection.length; i < length; i++) {
+ undoData.push({
+ entityID: selection[i].id,
+ properties: {
+ dimensions: selection[i].dimensions,
+ localPosition: selection[i].localPosition,
+ localRotation: selection[i].localRotation
+ }
+ });
+ selection[i].dimensions = Vec3.multiply(scaleFactor, selection[i].dimensions);
+ selection[i].localPosition = Vec3.multiply(scaleFactor, selection[i].localPosition);
+ redoData.push({
+ entityID: selection[i].id,
+ properties: {
+ dimensions: selection[i].dimensions,
+ localPosition: selection[i].localPosition,
+ localRotation: selection[i].localRotation
+ }
+ });
+ }
+
+ // Add history entry.
+ History.push(side, { setProperties: undoData }, { setProperties: redoData });
+
+ // Update grab start data for its undo.
+ startPosition = rootPosition;
+ startOrientation = rootOrientation;
+ }
+
+ function startHandleScaling(position) {
+ // Save initial offset from hand position to root position so that can scale without accumulating float errors.
+ scaleRootOffset = Vec3.multiplyQbyV(Quat.inverse(rootOrientation), Vec3.subtract(rootPosition, position));
+
+ // User is grabbing entity; add a history entry for movement up until the start of scaling and update start position and
+ // orientation; unless very small movement.
+ if (Vec3.distance(startPosition, rootPosition) >= MIN_HISTORY_MOVE_DISTANCE
+ || Quat.rotationBetween(startOrientation, rootOrientation) >= MIN_HISTORY_ROTATE_ANGLE) {
+ History.push(
+ side,
+ {
+ setProperties: [
+ { entityID: rootEntityID, properties: { position: startPosition, rotation: startOrientation } }
+ ]
+ },
+ {
+ setProperties: [
+ { entityID: rootEntityID, properties: { position: rootPosition, rotation: rootOrientation } }
+ ]
+ }
+ );
+ startPosition = rootPosition;
+ startOrientation = rootOrientation;
+ }
+ }
+
+ function handleScale(factor, position, orientation) {
+ // Scale and reposition and orient selection.
+ // We can get away with scaling the z size of 2D entities - incongruities are barely noticeable and things recover.
+ var i,
+ length;
+
+ // Scale and position root.
+ rootPosition = Vec3.sum(Vec3.multiplyQbyV(orientation, Vec3.multiplyVbyV(factor, scaleRootOffset)), position);
+ rootOrientation = orientation;
+ Entities.editEntity(selection[0].id, {
+ dimensions: Vec3.multiplyVbyV(factor, selection[0].dimensions),
+ position: rootPosition,
+ rotation: rootOrientation
+ });
+
+ // Scale and position children.
+ // Only corner handles are used for scaling multiple entities so scale factor is the same in all dimensions.
+ // Therefore don't need to take into account orientation relative to parent when scaling local position.
+ for (i = 1, length = selection.length; i < length; i++) {
+ Entities.editEntity(selection[i].id, {
+ dimensions: Vec3.multiplyVbyV(factor, selection[i].dimensions),
+ localPosition: Vec3.multiplyVbyV(factor, selection[i].localPosition),
+ localRotation: selection[i].localRotation // Always specify localRotation otherwise rotations can drift.
+ });
+ }
+
+ // Save most recent scale factor.
+ scaleFactor = factor;
+ }
+
+ function finishHandleScaling() {
+ // Update selection with final entity properties.
+ var undoData = [],
+ redoData = [],
+ i,
+ length;
+
+ // Final scale and position of root.
+ undoData.push({
+ entityID: selection[0].id,
+ properties: {
+ dimensions: selection[0].dimensions,
+ position: startPosition,
+ rotation: startOrientation
+ }
+ });
+ selection[0].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[0].dimensions);
+ selection[0].position = rootPosition;
+ selection[0].rotation = rootOrientation;
+ redoData.push({
+ entityID: selection[0].id,
+ properties: {
+ dimensions: selection[0].dimensions,
+ position: selection[0].position,
+ rotation: selection[0].rotation
+ }
+ });
+
+ // Final scale and position of children.
+ for (i = 1, length = selection.length; i < length; i++) {
+ undoData.push({
+ entityID: selection[i].id,
+ properties: {
+ dimensions: selection[i].dimensions,
+ localPosition: selection[i].localPosition,
+ localRotation: selection[i].localRotation
+ }
+ });
+ selection[i].dimensions = Vec3.multiplyVbyV(scaleFactor, selection[i].dimensions);
+ selection[i].localPosition = Vec3.multiplyVbyV(scaleFactor, selection[i].localPosition);
+ redoData.push({
+ entityID: selection[i].id,
+ properties: {
+ dimensions: selection[i].dimensions,
+ localPosition: selection[i].localPosition,
+ localRotation: selection[i].localRotation
+ }
+ });
+ }
+
+ // Add history entry.
+ History.push(side, { setProperties: undoData }, { setProperties: redoData });
+
+ // Update grab start data for its undo.
+ startPosition = rootPosition;
+ startOrientation = rootOrientation;
+ }
+
+ function cloneEntities() {
+ var parentIDIndexes = [],
+ intersectedEntityIndex = 0,
+ parentID,
+ properties,
+ undoData = [],
+ redoData = [],
+ i,
+ j,
+ length;
+
+ // Map parent IDs; find intersectedEntityID's index.
+ for (i = 1, length = selection.length; i < length; i++) {
+ if (selection[i].id === intersectedEntityID) {
+ intersectedEntityIndex = i;
+ }
+ parentID = selection[i].parentID;
+ for (j = 0; j < i; j++) {
+ if (parentID === selection[j].id) {
+ parentIDIndexes[i] = j;
+ break;
+ }
+ }
+ }
+
+ // Clone entities.
+ for (i = 0, length = selection.length; i < length; i++) {
+ properties = Entities.getEntityProperties(selection[i].id);
+ if (i > 0) {
+ properties.parentID = selection[parentIDIndexes[i]].id;
+ }
+ selection[i].id = Entities.addEntity(properties);
+ undoData.push({ entityID: selection[i].id });
+ redoData.push({ entityID: selection[i].id, properties: properties });
+ }
+
+ // Update selection info.
+ intersectedEntityID = selection[intersectedEntityIndex].id;
+ rootEntityID = selection[0].id;
+
+ // Add history entry.
+ History.prePush(side, { deleteEntities: undoData }, { createEntities: redoData });
+ }
+
+ function applyColor(color, isApplyToAll) {
+ // Entities without a color property simply ignore the edit.
+ var properties,
+ isOK = false,
+ undoData = [],
+ redoData = [],
+ i,
+ length;
+
+ if (isApplyToAll) {
+ for (i = 0, length = selection.length; i < length; i++) {
+ properties = Entities.getEntityProperties(selection[i].id, ["type", "color"]);
+ if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) {
+ Entities.editEntity(selection[i].id, {
+ color: color
+ });
+ undoData.push({ entityID: intersectedEntityID, properties: { color: properties.color } });
+ redoData.push({ entityID: intersectedEntityID, properties: { color: color } });
+ isOK = true;
+ }
+ }
+ if (undoData.length > 0) {
+ History.push(side, { setProperties: undoData }, { setProperties: redoData });
+ }
+ } else {
+ properties = Entities.getEntityProperties(intersectedEntityID, ["type", "color"]);
+ if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) !== -1) {
+ Entities.editEntity(intersectedEntityID, {
+ color: color
+ });
+ History.push(
+ side,
+ { setProperties: [{ entityID: intersectedEntityID, properties: { color: properties.color } }] },
+ { setProperties: [{ entityID: intersectedEntityID, properties: { color: color } }] }
+ );
+ isOK = true;
+ }
+ }
+
+ return isOK;
+ }
+
+ function getColor(entityID) {
+ var properties;
+
+ properties = Entities.getEntityProperties(entityID, "color");
+ if (ENTITY_TYPES_WITH_COLOR.indexOf(properties.type) === -1) {
+ // Some entities don't have a color property.
+ return null;
+ }
+
+ return properties.color;
+ }
+
+ function updatePhysicsUserData(userDataString, physicsUserData) {
+ var userData = {};
+
+ if (userDataString !== "") {
+ try {
+ userData = JSON.parse(userDataString);
+ } catch (e) {
+ App.log(side, "ERROR: Invalid userData in entity being updated! " + userDataString);
+ }
+ }
+
+ if (!userData.hasOwnProperty("grabbableKey")) {
+ userData.grabbableKey = {};
+ }
+ userData.grabbableKey.grabbable = physicsUserData.grabbableKey.grabbable;
+
+ return JSON.stringify(userData);
+ }
+
+ function applyPhysics(physicsProperties) {
+ // Regarding trees of entities, when physics is to be enabled the physics engine currently:
+ // - Only works with physics applied to the root entity; i.e., child entities are ignored for collisions.
+ // - Requires child entities to be dynamic if the root entity is dynamic, otherwise child entities can drift.
+ // - Requires child entities to be collisionless, otherwise the entity tree can become self-propelled.
+ // See also: Groups.group() and ungroup().
+ var properties,
+ property,
+ undoData = [],
+ redoData = [],
+ i,
+ length;
+
+ // Make children cater to physicsProperties.
+ properties = {
+ dynamic: physicsProperties.dynamic,
+ collisionless: physicsProperties.dynamic || physicsProperties.collisionless
+ };
+ for (i = 1, length = selection.length; i < length; i++) {
+ undoData.push({
+ entityID: selection[i].id,
+ properties: {
+ dynamic: selection[i].dynamic,
+ collisionless: selection[i].collisionless
+ }
+ });
+ Entities.editEntity(selection[i].id, properties);
+ undoData.push({
+ entityID: selection[i].id,
+ properties: properties
+ });
+ }
+
+ // Undo data.
+ properties = {
+ position: selection[0].position,
+ rotation: selection[0].rotation,
+ velocity: Vec3.ZERO,
+ angularVelocity: Vec3.ZERO
+ };
+ for (property in physicsProperties) {
+ if (physicsProperties.hasOwnProperty(property)) {
+ properties[property] = selectionProperties[0].properties[property];
+ }
+ }
+ if (properties.userData === undefined) {
+ properties.userData = "";
+ }
+ undoData.push({
+ entityID: selection[0].id,
+ properties: properties
+ });
+
+ // Set root per physicsProperties.
+ properties = Object.clone(physicsProperties);
+ properties.userData = updatePhysicsUserData(selection[intersectedEntityIndex].userData, physicsProperties.userData);
+ Entities.editEntity(rootEntityID, properties);
+
+ // Redo data.
+ properties.position = selection[0].position;
+ properties.rotation = selection[0].rotation;
+ properties.velocity = Vec3.ZERO;
+ properties.angularVelocity = Vec3.ZERO;
+ redoData.push({
+ entityID: selection[0].id,
+ properties: properties
+ });
+
+ // Add history entry.
+ History.push(side, { setProperties: undoData }, { setProperties: redoData });
+
+ // Kick off physics if necessary.
+ if (physicsProperties.dynamic) {
+ kickPhysics(rootEntityID);
+ }
+ }
+
+ function clear() {
+ selection = [];
+ intersectedEntityID = null;
+ rootEntityID = null;
+ }
+
+ function deleteEntities() {
+ if (rootEntityID) {
+ History.push(side, { createEntities: selectionProperties }, { deleteEntities: [{ entityID: rootEntityID }] });
+ Entities.deleteEntity(rootEntityID); // Children are automatically deleted.
+ clear();
+ }
+ }
+
+ function destroy() {
+ clear();
+ }
+
+ return {
+ select: select,
+ append: append,
+ selection: getSelection,
+ contains: contains,
+ count: count,
+ intersectedEntityID: getIntersectedEntityID,
+ intersectedEntityIndex: getIntersectedEntityIndex,
+ rootEntityID: getRootEntityID,
+ boundingBox: getBoundingBox,
+ is2D: is2D,
+ getPositionAndOrientation: getPositionAndOrientation,
+ setPositionAndOrientation: setPositionAndOrientation,
+ startEditing: startEditing,
+ startDirectScaling: startDirectScaling,
+ directScale: directScale,
+ finishDirectScaling: finishDirectScaling,
+ startHandleScaling: startHandleScaling,
+ handleScale: handleScale,
+ finishHandleScaling: finishHandleScaling,
+ finishEditing: finishEditing,
+ cloneEntities: cloneEntities,
+ applyColor: applyColor,
+ getColor: getColor,
+ applyPhysics: applyPhysics,
+ deleteEntities: deleteEntities,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+SelectionManager.prototype = {};
diff --git a/scripts/shapes/modules/toolIcon.js b/scripts/shapes/modules/toolIcon.js
new file mode 100644
index 0000000000..1571a6b037
--- /dev/null
+++ b/scripts/shapes/modules/toolIcon.js
@@ -0,0 +1,164 @@
+//
+// toolIcon.js
+//
+// Created by David Rowe on 28 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global ToolIcon:true, App, UIT */
+
+ToolIcon = function (side) {
+ // Tool icon displayed on non-dominant hand.
+
+ "use strict";
+
+ var LEFT_HAND = 0,
+
+ MODEL_DIMENSIONS = { x: 0.1944, y: 0.1928, z: 0.1928 }, // Raw FBX dimensions.
+ MODEL_SCALE = 0.7, // Adjust icon dimensions so that the green bar matches that of the Tools header.
+ MODEL_POSITION_LEFT_HAND = { x: -0.025, y: 0.03, z: 0 }, // x raises in thumb direction; y moves in fingers direction.
+ MODEL_POSITION_RIGHT_HAND = { x: 0.025, y: 0.03, z: 0 }, // ""
+ MODEL_ROTATION_LEFT_HAND = Quat.fromVec3Degrees({ x: 0, y: 0, z: 100 }),
+ MODEL_ROTATION_RIGHT_HAND = Quat.fromVec3Degrees({ x: 0, y: 180, z: -100 }),
+
+ MODEL_TYPE = "model",
+ MODEL_PROPERTIES = {
+ url: Script.resolvePath("../assets/tools/tool-icon.fbx"),
+ dimensions: Vec3.multiply(MODEL_SCALE, MODEL_DIMENSIONS),
+ solid: true,
+ alpha: 1.0,
+ parentID: Uuid.SELF,
+ ignoreRayIntersection: true,
+ visible: true
+ },
+
+ IMAGE_TYPE = "image3d",
+ IMAGE_PROPERTIES = {
+ localRotation: Quat.fromVec3Degrees({ x: -90, y: -90, z: 0 }),
+ alpha: 1.0,
+ emissive: true,
+ ignoreRayIntersection: true,
+ isFacingAvatar: false,
+ visible: true
+ },
+
+ ICON_PROPERTIES = {
+ // Relative to model overlay. x is in fingers direction; y is in thumb direction.
+ localPosition: { x: 0.020, y: 0.069, z: 0 },
+ color: UIT.colors.lightGrayText
+ },
+ LABEL_PROPERTIES = {
+ localPosition: { x: -0.040, y: 0.067, z: 0 },
+ color: UIT.colors.white
+ },
+ SUBLABEL_PROPERTIES = {
+ localPosition: { x: -0.055, y: 0.067, z: 0 },
+ color: UIT.colors.lightGrayText
+ },
+
+ ICON_SCALE_FACTOR = 3.0,
+ LABEL_SCALE_FACTOR = 1.8,
+
+ handJointName,
+ localPosition,
+ localRotation,
+
+ modelOverlay = null;
+
+ if (!(this instanceof ToolIcon)) {
+ return new ToolIcon();
+ }
+
+ function setHand(side) {
+ // Assumes UI is not displaying.
+ if (side === LEFT_HAND) {
+ handJointName = "LeftHand";
+ localPosition = MODEL_POSITION_LEFT_HAND;
+ localRotation = MODEL_ROTATION_LEFT_HAND;
+ } else {
+ handJointName = "RightHand";
+ localPosition = MODEL_POSITION_RIGHT_HAND;
+ localRotation = MODEL_ROTATION_RIGHT_HAND;
+ }
+ }
+
+ setHand(side);
+
+ function clear() {
+ // Deletes current tool model.
+ if (modelOverlay) {
+ Overlays.deleteOverlay(modelOverlay); // Child overlays are automatically deleted.
+ modelOverlay = null;
+ }
+ }
+
+ function display(iconInfo) {
+ // Displays icon on hand.
+ var handJointIndex,
+ properties;
+
+ handJointIndex = MyAvatar.getJointIndex(handJointName);
+ if (handJointIndex === -1) {
+ // Don't display if joint isn't available (yet) to attach to.
+ // User can clear this condition by toggling the app off and back on once avatar finishes loading.
+ App.log(side, "ERROR: ToolIcon: Hand joint index isn't available!");
+ return;
+ }
+
+ if (modelOverlay !== null) {
+ // Should never happen because tool needs to be cleared in order for user to return to Tools menu.
+ clear();
+ }
+
+ // Model.
+ properties = Object.clone(MODEL_PROPERTIES);
+ properties.parentJointIndex = handJointIndex;
+ properties.localPosition = localPosition;
+ properties.localRotation = localRotation;
+ modelOverlay = Overlays.addOverlay(MODEL_TYPE, properties);
+
+ // Icon.
+ properties = Object.clone(IMAGE_PROPERTIES);
+ properties = Object.merge(properties, ICON_PROPERTIES);
+ properties.parentID = modelOverlay;
+ properties.url = iconInfo.icon.properties.url;
+ properties.dimensions = {
+ x: ICON_SCALE_FACTOR * iconInfo.icon.properties.dimensions.x,
+ y: ICON_SCALE_FACTOR * iconInfo.icon.properties.dimensions.y
+ };
+ properties.localPosition.y += ICON_SCALE_FACTOR * iconInfo.icon.headerOffset.y;
+ Overlays.addOverlay(IMAGE_TYPE, properties);
+
+ // Label.
+ properties = Object.clone(IMAGE_PROPERTIES);
+ properties = Object.merge(properties, LABEL_PROPERTIES);
+ properties.parentID = modelOverlay;
+ properties.url = iconInfo.label.properties.url;
+ properties.scale = LABEL_SCALE_FACTOR * iconInfo.label.properties.scale;
+ Overlays.addOverlay(IMAGE_TYPE, properties);
+
+ // Sublabel.
+ properties = Object.clone(IMAGE_PROPERTIES);
+ properties = Object.merge(properties, SUBLABEL_PROPERTIES);
+ properties.parentID = modelOverlay;
+ properties.url = iconInfo.sublabel.properties.url;
+ properties.scale = LABEL_SCALE_FACTOR * iconInfo.sublabel.properties.scale;
+ Overlays.addOverlay(IMAGE_TYPE, properties);
+ }
+
+ function destroy() {
+ clear();
+ }
+
+ return {
+ setHand: setHand,
+ display: display,
+ clear: clear,
+ destroy: destroy
+ };
+};
+
+ToolIcon.prototype = {};
diff --git a/scripts/shapes/modules/toolsMenu.js b/scripts/shapes/modules/toolsMenu.js
new file mode 100644
index 0000000000..19c114c8e9
--- /dev/null
+++ b/scripts/shapes/modules/toolsMenu.js
@@ -0,0 +1,3694 @@
+//
+// toolsMenu.js
+//
+// Created by David Rowe on 22 Jul 2017.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global ToolsMenu: true, App, Feedback, UIT */
+
+ToolsMenu = function (side, leftInputs, rightInputs, uiCommandCallback) {
+ // Tool menu displayed on top of forearm.
+
+ "use strict";
+
+ var attachmentJointName,
+
+ menuOriginLocalPosition,
+ menuOriginLocalRotation,
+
+ menuOriginOverlay,
+ menuHeaderOverlay,
+ menuHeaderHeadingOverlay,
+ menuHeaderBarOverlay,
+ menuHeaderBackOverlay,
+ menuHeaderTitleOverlay,
+ menuHeaderIconOverlay,
+ menuPanelOverlay,
+
+ menuOverlays = [],
+ menuHoverOverlays = [],
+ menuIconOverlays = [],
+ menuLabelOverlays = [],
+ menuEnabled = [],
+
+ optionsOverlays = [],
+ optionsOverlaysIDs = [], // Text ids (names) of options overlays.
+ optionsOverlaysLabels = [], // Overlay IDs of labels for optionsOverlays.
+ optionsOverlaysSublabels = [], // Overlay IDs of sublabels for optionsOverlays.
+ optionsSliderData = [], // Uses same index values as optionsOverlays.
+ optionsColorData = [], // Uses same index values as optionsOverlays.
+ optionsExtraOverlays = [],
+ optionsEnabled = [],
+ optionsSettings = {
+ //