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 @@ + + + + CREATE + Created with Sketch. + + + + + + + \ 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 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + 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 @@ + + + + BACK + Created with Sketch. + + + + + + + \ 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 @@ + + + + back-icon + Created with Sketch. + + + + + + + + + \ 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 @@ + + + + clone-icon + Created with Sketch. + + + + + + + + + \ 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 @@ + + + + CLONE + Created with Sketch. + + + + + + + \ 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 @@ + + + + CLONE TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + color-icon + Created with Sketch. + + + + + + + + + + \ 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 @@ + + + + COLOR + Created with Sketch. + + + + + + + \ 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 @@ + + + + COLOR TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + noun_792623 + Created with Sketch. + + + + + + + + + \ 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 @@ + + + + SWATCHES + Created with Sketch. + + + + + + + \ 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 @@ + + + + ACTIONS + Created with Sketch. + + + + + + + \ 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 @@ + + + + 5 + Created with Sketch. + + + + + + + \ 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 @@ + + + + FINISH + Created with Sketch. + + + + + + + \ 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 @@ + + + + [ + Created with Sketch. + + + + + + + \ 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 @@ + + + + 5 copy + Created with Sketch. + + + + + + + \ 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 @@ + + + + delete-icon + Created with Sketch. + + + + + + + + + \ 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 @@ + + + + DELETE + Created with Sketch. + + + + + + + \ 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 @@ + + + + DELETE TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + group-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + GROUP + Created with Sketch. + + + + + + + \ 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 @@ + + + + GROUP TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + GROUP + Created with Sketch. + + + + + + + \ 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 @@ + + + + group-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + UNGROUP + Created with Sketch. + + + + + + + \ 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 @@ + + + + physics-icon + Created with Sketch. + + + + + + + + + + \ 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 @@ + + + + PHYSICS + Created with Sketch. + + + + + + + \ 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 @@ + + + + PHYSICS TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + COLLISIONS + Created with Sketch. + + + + + + + \ 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 @@ + + + + GRABBABLE + Created with Sketch. + + + + + + + \ 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 @@ + + + + GRAVITY Copy + Created with Sketch. + + + + + + + \ 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 @@ + + + + OFF + Created with Sketch. + + + + + + + \ 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 @@ + + + + ON + Created with Sketch. + + + + + + + \ 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 @@ + + + + PRESETS + Created with Sketch. + + + + + + + \ 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 @@ + + + + BALLOON + Created with Sketch. + + + + + + + \ 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 @@ + + + + COTTON + Created with Sketch. + + + + + + + \ 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 @@ + + + + CUSTOM … + Created with Sketch. + + + + + + + \ 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 @@ + + + + ICE + Created with Sketch. + + + + + + + \ 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 @@ + + + + LEAD Copy + Created with Sketch. + + + + + + + \ 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 @@ + + + + RUBBER + Created with Sketch. + + + + + + + \ 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 @@ + + + + TUMBLEWEED + Created with Sketch. + + + + + + + \ 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 @@ + + + + WOOD + Created with Sketch. + + + + + + + \ 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 @@ + + + + ZERO-G + Created with Sketch. + + + + + + + \ 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 @@ + + + + PROPERTIES + Created with Sketch. + + + + + + + \ 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 @@ + + + + BOUNCE + Created with Sketch. + + + + + + + \ 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 @@ + + + + DENSITY + Created with Sketch. + + + + + + + \ 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 @@ + + + + FRICTION + Created with Sketch. + + + + + + + \ 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 @@ + + + + GRAVITY + Created with Sketch. + + + + + + + \ 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 @@ + + + + stretch-icon + Created with Sketch. + + + + + + + + + \ 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 @@ + + + + STRETCH + Created with Sketch. + + + + + + + \ 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 @@ + + + + STRETCH TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + Stretch objects by g + Created with Sketch. + + + + + + + \ 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 @@ + + + + TOOL + Created with Sketch. + + + + + + + \ 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 @@ + + + + TOOLS + Created with Sketch. + + + + + + + \ 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 = { + //